├── .dockerignore ├── .env.example ├── .github └── workflows │ ├── main.yml │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── data ├── OpenBangla-Keyboard_2.0.0-fedora38.rpm ├── OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb ├── fastfetch-linux-aarch64.deb ├── fastfetch-linux-aarch64.rpm ├── fastfetch-linux-amd64.deb ├── fastfetch-linux-amd64.rpm ├── fastfetch-linux-armv6l.deb ├── fastfetch-linux-armv6l.rpm ├── fastfetch-linux-armv7l.deb ├── fastfetch-linux-armv7l.rpm ├── fastfetch-linux-ppc64le.deb ├── fastfetch-linux-ppc64le.rpm ├── fastfetch-linux-riscv64.deb ├── fastfetch-linux-riscv64.rpm ├── fastfetch-linux-s390x.deb ├── fastfetch-linux-s390x.rpm ├── fcitx-openbangla_3.0.0.deb ├── ibus-openbangla_3.0.0.deb └── self-signed-certs │ ├── cert.pem │ └── key.pem ├── docker-compose.yml ├── images ├── debian12-multi-package.Dockerfile ├── debian12.Dockerfile ├── fedora38-multi-package.Dockerfile ├── fedora38.Dockerfile ├── server-ci.Dockerfile ├── server.Dockerfile ├── tumbleweed-multi-package.Dockerfile ├── ubuntu24.04-multi-package.Dockerfile └── ubuntu24.04.Dockerfile ├── pages ├── assets │ ├── asciinema.svg │ ├── debian.svg │ ├── fedora.png │ ├── logo.svg │ ├── normalize.css │ ├── openSUSE.svg │ ├── packhub.css │ ├── packhub.js │ ├── rpm.svg │ └── ubuntu.png └── index.html ├── scripts ├── check_apt.sh ├── check_apt_multiple.sh ├── check_dnf.sh ├── check_dnf_multiple.sh ├── check_zypper_multiple.sh ├── deploy.ps1 ├── deploy.sh └── run_server.sh ├── src ├── apt │ ├── deb.rs │ ├── index.rs │ ├── mod.rs │ ├── routes.rs │ └── snapshots │ │ ├── packhub__apt__index__tests__apt_indices-2.snap │ │ ├── packhub__apt__index__tests__apt_indices.snap │ │ ├── packhub__apt__index__tests__multiple_architectures.snap │ │ ├── packhub__apt__index__tests__multiple_packages-2.snap │ │ └── packhub__apt__index__tests__multiple_packages.snap ├── db.rs ├── detect.rs ├── error.rs ├── lib.rs ├── main.rs ├── package.rs ├── pgp.rs ├── platform.rs ├── repository.rs ├── rpm │ ├── index.rs │ ├── mod.rs │ ├── package.rs │ ├── routes.rs │ └── snapshots │ │ ├── packhub__rpm__index__tests__multiple_arch-2.snap │ │ ├── packhub__rpm__index__tests__multiple_arch-3.snap │ │ ├── packhub__rpm__index__tests__multiple_arch-4.snap │ │ ├── packhub__rpm__index__tests__multiple_arch.snap │ │ ├── packhub__rpm__index__tests__rpm_indices-2.snap │ │ ├── packhub__rpm__index__tests__rpm_indices-3.snap │ │ ├── packhub__rpm__index__tests__rpm_indices-4.snap │ │ ├── packhub__rpm__index__tests__rpm_indices.snap │ │ ├── packhub__rpm__package__tests__parser-2.snap │ │ └── packhub__rpm__package__tests__parser.snap ├── script.rs ├── selector.rs ├── snapshots │ ├── packhub__script__tests__script_generation_apt.snap │ ├── packhub__script__tests__script_generation_rpm-2.snap │ └── packhub__script__tests__script_generation_rpm.snap ├── state.rs └── utils.rs └── templates ├── Packages ├── Release ├── apt-script.sh ├── filelists.xml ├── other.xml ├── primary.xml ├── repomd.xml └── rpm-script.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | images/ 3 | data/* 4 | !data/self-signed-certs/ 5 | .github/ 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PACKHUB_DOMAIN=http://localhost:3000 2 | PACKHUB_HTTP_PORT=3000 3 | PACKHUB_HTTPS_PORT=3443 4 | PACKHUB_CERT_PEM="data/self-signed-certs/cert.pem" 5 | PACKHUB_KEY_PEM="data/self-signed-certs/key.pem" 6 | PACKHUB_DOCKER_CERT_PEM="data/self-signed-certs/cert.pem" 7 | PACKHUB_DOCKER_KEY_PEM="data/self-signed-certs/key.pem" 8 | PACKHUB_DB_USER=root 9 | PACKHUB_DB_PASSWORD=pass 10 | PACKHUB_DB_HOST=localhost 11 | PACKHUB_SIGN_PASSPHRASE=passphrase 12 | PACKHUB_GITHUB_PAT="" 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | test: 7 | name: Test Suite 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: dtolnay/rust-toolchain@stable 12 | - run: sudo apt update && sudo apt install -y clang llvm pkg-config nettle-dev 13 | - run: cp .env.example .env 14 | - run: cargo test 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Linux Distribution Test Pipeline 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | services: 10 | mongodb: 11 | image: mongo:5.0.6 12 | ports: 13 | - 27017:27017 14 | options: >- 15 | --health-cmd mongo 16 | --health-interval 10s 17 | --health-timeout 5s 18 | --health-retries 5 19 | env: 20 | MONGO_INITDB_ROOT_USERNAME: root 21 | MONGO_INITDB_ROOT_PASSWORD: pass 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup .env file 28 | run: | 29 | cp .env.example .env 30 | 31 | - name: Build and run server container 32 | run: | 33 | docker build -t server -f images/server-ci.Dockerfile . 34 | docker run -d --network host --name server-container server 35 | 36 | - name: Wait for server to start 37 | run: | 38 | TIMEOUT=60 39 | ELAPSED=0 40 | 41 | until curl --output /dev/null --silent --fail http://localhost:3000; do 42 | if [ "$ELAPSED" -ge "$TIMEOUT" ]; then 43 | echo "Server did not start within $TIMEOUT seconds. But continuing..." 44 | exit 0 45 | fi 46 | echo "Waiting for server..." 47 | sleep 5 48 | ELAPSED=$((ELAPSED + 5)) 49 | done 50 | echo "Server is up!" 51 | continue-on-error: true 52 | 53 | - name: Build and run Ubuntu 23.04 container 54 | run: | 55 | docker build -t ubuntu24.04 -f images/ubuntu24.04.Dockerfile scripts/ 56 | docker run --name ubuntu24.04-test --network host ubuntu24.04 || true 57 | 58 | - name: Build and run Fedora 38 container 59 | run: | 60 | docker build -t fedora38 -f images/fedora38.Dockerfile scripts/ 61 | docker run --name fedora38-test --network host fedora38 || true 62 | 63 | - name: Build and run Debian 12 container 64 | run: | 65 | docker build -t debian12 -f images/debian12.Dockerfile scripts/ 66 | docker run --name debian12-test --network host debian12 || true 67 | 68 | - name: Build and run Ubuntu 23.04 container for checking multiple package support 69 | run: | 70 | docker build -t ubuntu24.04-multi -f images/ubuntu24.04-multi-package.Dockerfile scripts/ 71 | docker run --name ubuntu24.04-multitest --network host ubuntu24.04-multi || true 72 | 73 | - name: Build and run Fedora 38 container for checking multiple package support 74 | run: | 75 | docker build -t fedora38 -f images/fedora38-multi-package.Dockerfile scripts/ 76 | docker run --name fedora38-multitest --network host fedora38 || true 77 | 78 | - name: Build and run Debian 12 container for checking multiple package support 79 | run: | 80 | docker build -t debian12-multi -f images/debian12-multi-package.Dockerfile scripts/ 81 | docker run --name debian12-multitest --network host debian12-multi || true 82 | 83 | - name: Build and run OpenSuse Tumbleweed container for checking multiple package support 84 | run: | 85 | docker build -t tumbleweed-multi -f images/tumbleweed-multi-package.Dockerfile scripts/ 86 | docker run --name tumbleweed-multitest --network host tumbleweed-multi || true 87 | 88 | - name: Check server logs 89 | run: | 90 | docker logs server-container || true 91 | 92 | - name: Check client containers' statuses 93 | run: | 94 | if [ "$(docker inspect -f '{{.State.ExitCode}}' ubuntu24.04-test)" -ne 0 ]; then 95 | echo "Test on Ubuntu 23.04 failed" 96 | exit 1 97 | fi 98 | 99 | if [ "$(docker inspect -f '{{.State.ExitCode}}' fedora38-test)" -ne 0 ]; then 100 | echo "Test on Fedora 38 failed" 101 | exit 1 102 | fi 103 | 104 | if [ "$(docker inspect -f '{{.State.ExitCode}}' debian12-test)" -ne 0 ]; then 105 | echo "Test on Debian 12 failed" 106 | exit 1 107 | fi 108 | 109 | if [ "$(docker inspect -f '{{.State.ExitCode}}' ubuntu24.04-multitest)" -ne 0 ]; then 110 | echo "Test on Ubuntu 23.04 for multiple package support failed" 111 | exit 1 112 | fi 113 | 114 | if [ "$(docker inspect -f '{{.State.ExitCode}}' fedora38-multitest)" -ne 0 ]; then 115 | echo "Test on Fedora 38 failed multiple package support failed" 116 | exit 1 117 | fi 118 | 119 | if [ "$(docker inspect -f '{{.State.ExitCode}}' debian12-multitest)" -ne 0 ]; then 120 | echo "Test on Debian 12 for multiple package support failed" 121 | exit 1 122 | fi 123 | 124 | if [ "$(docker inspect -f '{{.State.ExitCode}}' tumbleweed-multitest)" -ne 0 ]; then 125 | echo "Test on OpenSuse Tumbleweed for multiple package support failed" 126 | exit 1 127 | fi 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | packhub.asc 13 | secret_key.asc 14 | .env 15 | key.gpg 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "packhub" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | axum = { version = "0.8", features = ["macros"] } 10 | axum-extra = { version = "0.10", features = ["typed-header"] } 11 | axum-server = { version = "0.7", features = ["tls-rustls"] } 12 | askama = "0.13" 13 | chrono = { version = "0.4.38", features = ["clock"] } 14 | dotenvy = "0.15" 15 | octocrab = "0.44" 16 | regex = "1" 17 | semver = "1" 18 | lenient_semver = "0.4" 19 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 20 | tower-http = { version = "0.6", features = ["trace", "fs"] } 21 | tracing = "0.1" 22 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 23 | reqwest = { version = "0.12", features = ["stream", "rustls-tls"] } 24 | rustls = "0.23" 25 | ar = "0.9" 26 | libflate = "2" 27 | tar = "0.4" 28 | sha1 = "0.10" 29 | sha2 = "0.10" 30 | md-5 = "0.10" 31 | rpm = "0.17" 32 | anyhow = "1" 33 | zstd = "0.13" 34 | mongodb = "3" 35 | bson = { version = "2", features = ["chrono-0_4"] } 36 | serde = { version = "1", features = ["derive"] } 37 | serde_json = "1" 38 | sequoia-openpgp = "2.0.0" 39 | 40 | [dev-dependencies] 41 | insta = { version = "1", features = ["filters"] } 42 | testcontainers-modules = { version = "0.11", features = ["mongo"] } 43 | 44 | [profile.dev.package.insta] 45 | opt-level = 3 46 | 47 | [profile.dev.package.similar] 48 | opt-level = 3 49 | 50 | [profile.release] 51 | codegen-units = 1 52 | lto = true 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PackHub - Decentralized Package Repositories for Linux 2 | [![Build Status](https://github.com/mominul/packhub/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/mominul/packhub/actions?query=branch%3Amain) 3 | [![Rust](https://img.shields.io/badge/rust-1.85.1%2B-blue.svg?maxAge=3600)](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html) 4 | [![asciicast](/pages/assets/asciinema.svg)](https://asciinema.org/a/ncMOerw3L7RwhTXqA3Ck7T7En) 5 | ## 🚀 Install Linux Packages Directly from GitHub! 6 | PackHub dynamically creates virtual Linux package repositories (`apt`, `dnf`, `yum`, etc.) on the fly, pulling directly from GitHub Releases. No need for centralized repositories—just seamless installations. 7 | 8 | > [!TIP] 9 | > This project is powered by github 🌟s. Go ahead and star it please! 10 | 11 | ## ✨ Features 12 | 13 | - **Decentralized Package Management** – Install packages directly from GitHub Releases. 14 | - **Seamless Updates** – Automatically fetches the latest releases and updates your package manager. 15 | - **Smart Versioning** – Detects your system version and selects the most compatible package. 16 | - **Developer Freedom** – No need to maintain separate repositories or rely on a maintainer. 17 | - **User Empowerment** – Get the apps you need instantly, without waiting for repositories or manual downloads. 18 | 19 | ## 🚀 Usage 20 | 21 | To install Linux packages from a GitHub repository using PackHub, the repository must have published Linux packages in its Releases. You'll also need to set up the PackHub repository in your system's package manager. 22 | 23 | Replace `OWNER` with the repository owner's name and `REPO` with the repository name. For example, for `https://github.com/sindresorhus/caprine`, use `sindresorhus` as `OWNER` and `caprine` as `REPO`. 24 | 25 | If you're unsure, visit [packhub.dev](https://packhub.dev) to generate the correct command for your repository. 26 | 27 | ### Ubuntu-Based Distributions 28 | ```bash 29 | wget -qO- http://packhub.dev/sh/ubuntu/github/OWNER/REPO | sh 30 | ``` 31 | 32 | ### Debian-Based Distributions 33 | ```bash 34 | wget -qO- http://packhub.dev/sh/debian/github/OWNER/REPO | sh 35 | ``` 36 | 37 | ### Fedora 38 | ```bash 39 | wget -qO- http://packhub.dev/sh/yum/github/OWNER/REPO | sh 40 | ``` 41 | 42 | ### openSUSE 43 | ```bash 44 | wget -qO- http://packhub.dev/sh/zypp/github/OWNER/REPO | sh 45 | ``` 46 | 47 | Once the PackHub repository is set up, you can install packages using your system’s package manager (`apt`, `dnf`, `yum`, etc.). 48 | 49 | ## 🔧 Built With 50 | 51 | - [**Rust**](https://www.rust-lang.org/) – Ensuring performance, safety, and concurrency. 52 | - [**Axum**](https://crates.io/crates/axum) – A powerful, async web framework for Rust. 53 | - [**Repology**](https://repology.org/) - Leverages its API to gather `apt` package versions from Ubuntu and Debian repositories. 54 | - [**octocrab**](https://crates.io/crates/octocrab) - A modern, extensible GitHub API client. 55 | - [**rpm**](https://crates.io/crates/rpm) - A pure rust library for building and parsing RPMs. 56 | - [**sequoia-openpgp**](https://crates.io/crates/sequoia-openpgp) - OpenPGP key generation and message signing. 57 | 58 | Additional dependencies can be found in the `Cargo.toml` file. 59 | 60 | ## 🤝 Contributing 61 | We welcome contributions! To get started: 62 | 1. Fork the repository. 63 | 2. Create a new branch (`git checkout -b feature-branch`). 64 | 3. Commit your changes (`git commit -m 'Add new feature'`). 65 | 4. Push to the branch (`git push origin feature-branch`). 66 | 5. Open a Pull Request. 67 | 68 | ## 🤗 Acknowledgement 69 | 70 | Special thanks to **Md. Asadujjaman Noor** ([@gold-4N](https://github.com/gold-4N/)) for providing valuable guidance on OpenPGP key generation and the signing process, as well as facilitating a discount from the hosting provider! 71 | 72 | 73 | 74 | ## 📄 License 75 | PackHub is licensed under the [GPLv3 License](LICENSE). 76 | 77 | Made with ❤️ by [Muhammad Mominul Huque](https://github.com/mominul) and ✨ [contributors](https://github.com/mominul/packhub/graphs/contributors) ✨! 78 | -------------------------------------------------------------------------------- /data/OpenBangla-Keyboard_2.0.0-fedora38.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/OpenBangla-Keyboard_2.0.0-fedora38.rpm -------------------------------------------------------------------------------- /data/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-aarch64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-aarch64.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-aarch64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-aarch64.rpm -------------------------------------------------------------------------------- /data/fastfetch-linux-amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-amd64.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-amd64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-amd64.rpm -------------------------------------------------------------------------------- /data/fastfetch-linux-armv6l.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-armv6l.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-armv6l.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-armv6l.rpm -------------------------------------------------------------------------------- /data/fastfetch-linux-armv7l.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-armv7l.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-armv7l.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-armv7l.rpm -------------------------------------------------------------------------------- /data/fastfetch-linux-ppc64le.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-ppc64le.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-ppc64le.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-ppc64le.rpm -------------------------------------------------------------------------------- /data/fastfetch-linux-riscv64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-riscv64.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-riscv64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-riscv64.rpm -------------------------------------------------------------------------------- /data/fastfetch-linux-s390x.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-s390x.deb -------------------------------------------------------------------------------- /data/fastfetch-linux-s390x.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fastfetch-linux-s390x.rpm -------------------------------------------------------------------------------- /data/fcitx-openbangla_3.0.0.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/fcitx-openbangla_3.0.0.deb -------------------------------------------------------------------------------- /data/ibus-openbangla_3.0.0.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/data/ibus-openbangla_3.0.0.deb -------------------------------------------------------------------------------- /data/self-signed-certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDxzCCAq+gAwIBAgIUVxxOrrUqRjltR7RE4NGjGy6VD3QwDQYJKoZIhvcNAQEL 3 | BQAwgYoxCzAJBgNVBAYTAkJOMQ4wDAYDVQQIDAVEaGFrYTEPMA0GA1UEBwwGTWly 4 | cHVyMRAwDgYDVQQKDAdwYWNraHViMRAwDgYDVQQLDAdzaWduaW5nMRAwDgYDVQQD 5 | DAdwYWNraHViMSQwIgYJKoZIhvcNAQkBFhVtb21pbnVsMjA4MkBnbWFpbC5jb20w 6 | IBcNMjUwMzI2MjA1NDE1WhgPMjEyNTAzMDIyMDU0MTVaMIGKMQswCQYDVQQGEwJC 7 | TjEOMAwGA1UECAwFRGhha2ExDzANBgNVBAcMBk1pcnB1cjEQMA4GA1UECgwHcGFj 8 | a2h1YjEQMA4GA1UECwwHc2lnbmluZzEQMA4GA1UEAwwHcGFja2h1YjEkMCIGCSqG 9 | SIb3DQEJARYVbW9taW51bDIwODJAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEF 10 | AAOCAQ8AMIIBCgKCAQEAzwadcdd8y8edK+BS3UjBKvPMil0+nkzODd38PJYCaQ5F 11 | qawRWPt2ZG781Qv8EGLdbac56Lr4KIWYtCpKPzGvWhD0YZUNtOCDMP30SlE/O20I 12 | Eri+QiEPDFVztkM5QqDmqzd6b8Q61aI/NNVfA/QKoPYeWfjNPYx/kwCVpSYglsmX 13 | yDHfR1GNmmb/kYs1D7YZUTAuOi4IIlfDmdTaKueWm7SCz/TV0QX69t3i9vOzHKW1 14 | yR0MuwJkuX09N7qgOqpmsr5xN4OgBYVgkl/EMsTafwF4FCUuc+XXOZkNPLfpArCD 15 | VIEMslqFOlCgKb4qfVA0otUhIHPrhhC/tHCSiqBSTQIDAQABoyEwHzAdBgNVHQ4E 16 | FgQUTl3ZqDB2BpNhjHTg2BnVtx2PDPwwDQYJKoZIhvcNAQELBQADggEBAJqiOalN 17 | UV4cBm08F5rayzm8Jt62Oc4vs2iwkFxmrOlTppYcwOhD7uywlwpDvtVE8nibkiE0 18 | Xa9xvCQAp2sqfWefUYezJw6U5lY19LYnuQQhZgKo6Ry3JEaEYQMyXzV9riGHhpD0 19 | roXN9UbIJt71dyidv6/elaJsgyetpFAv7dAiSXnBNW2YNPToOxya7qunesoJU0yN 20 | zsIbIWrdv6J68QgZOVucK9RewWTF3hlRDXvw1+6fG6+/zEXRy/X75AnrgRFaVsZs 21 | ZvikeiHqEJe/Rlz7NxAuinIv2s+auZdor9JTuCV7dOq6WLfZiCQYXUc6X5IZP6sE 22 | OoflzX8c/kDcFtI= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /data/self-signed-certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPBp1x13zLx50r 3 | 4FLdSMEq88yKXT6eTM4N3fw8lgJpDkWprBFY+3ZkbvzVC/wQYt1tpznouvgohZi0 4 | Kko/Ma9aEPRhlQ204IMw/fRKUT87bQgSuL5CIQ8MVXO2QzlCoOarN3pvxDrVoj80 5 | 1V8D9Aqg9h5Z+M09jH+TAJWlJiCWyZfIMd9HUY2aZv+RizUPthlRMC46LggiV8OZ 6 | 1Noq55abtILP9NXRBfr23eL287McpbXJHQy7AmS5fT03uqA6qmayvnE3g6AFhWCS 7 | X8QyxNp/AXgUJS5z5dc5mQ08t+kCsINUgQyyWoU6UKApvip9UDSi1SEgc+uGEL+0 8 | cJKKoFJNAgMBAAECggEAWDJpQOacs/QGcXrP0pX8NWBH82pmEuqFnkLEAsulmzwJ 9 | UY+MlGwMtBzUea7xY8m6q8xiT1PYBOtlctvRZbq1CZnPgwMNI9HCEk0elcqnNZnt 10 | powuAd4zmv2MnkllS41gt/CaqKLgrcLBSrDcGcMOBCTWKV5lkaMZdnb5SbJEj/ft 11 | crnpXxO8aMY5LXvwpWNYtCKZEzegqRlnI0RqVgYYDDMA+4+DYHBhrnw3E/Ev8F0R 12 | yB4kQ+KN9ZC8fL48+xa9QjWJd9KqIx4U08of7YiFxVuCPJTHWGY6nAYjZvNZROUj 13 | dqlstDKOQXaWgKo+qOeO97MRPNENPfhVZ8OjQFx3wQKBgQD5zmRXq90+G3Z9DVnP 14 | YU9CBOoF6rEXRmBD/bSLZDkNHYFDGWYVFKW2D/ehjIdoIZ3UWVU/fofcuwQghoRb 15 | JPBrGWlxSk5Fa4536G8hP4WaWYBSkZeUrp16B9dBWxO9QKeBOSIpXGPh28CS66U1 16 | TybqfB389VD+rmh4BK7ITTu5ZwKBgQDUKK5RPnhQHVtLBydKrOXnqQjcQ5k4JrGq 17 | L0/csNGeArhWBoHaQ09g7tc9mZYRdUNsUY9afpR7KACmB/aCwxj4lTLba9mlOJ+x 18 | fFMKma37ghBCxtpwIeAtDfREpW4ueEHaY25oaz4Pmcl8q/j7dbI9ZQva0S9zFho9 19 | H3aTe3OiKwKBgH4UOja5ilePWtUwyNRPI8aJXmgQFMNPhMSsJtR3iAfjjVsFVa1s 20 | F1r1YiFKIQlgdh033TvHq+CvDx0vZ1vtH96eG8bPHwQQjf5c9MHOIqtNYuPJkby8 21 | CMUPcggNZMAPArvIz0Ia3FqhI+fDQUXPpi+Q5z3FvtRbyGRS0LhNqsgfAoGAEDZL 22 | m8m6R+T2ZPVW+03bA6jXFH3V54SNbwPOhn68heaPT4OPyK38EtwtdneWEB9114Ek 23 | AzZJAmA8LHPPUo62Ccjc6geDyixZh6aIcfbsZJu7wl6PsqHkD41RbS13DfYCkj2m 24 | 4jPPukF2NCCwFgcYZ7ig/0ec6J53wtP0q7BzVaECgYEA92OfLTgAIZaPKwVcJ8h+ 25 | ssIR0TNY68xwvD/r5MygkL6FZNIBlIvHjszQvdiE0QSqEJKOKZaMDrK4o4gZ2rPS 26 | UzuupVqsbWZlNwrNRPQN4LIaco+mddKvNLbNcwN6fD202qApixUQTfi1ZjcjP+Ms 27 | k0Lg0sylA+MuqwOjceIWrck= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | image: mominul/packhub:latest 4 | container_name: packhub_app 5 | depends_on: 6 | - mongodb 7 | ports: 8 | - "80:80" 9 | - "443:443" 10 | volumes: 11 | - ./.env:/app/.env 12 | - ./key.gpg:/app/key.gpg 13 | - ${PACKHUB_DOCKER_CERT_PEM}:/app/fullchain.pem 14 | - ${PACKHUB_DOCKER_KEY_PEM}:/app/privkey.pem 15 | 16 | mongodb: 17 | image: mongo:5.0.6 18 | container_name: packhub_mongodb 19 | restart: always 20 | ports: 21 | - "27017:27017" 22 | environment: 23 | MONGO_INITDB_ROOT_USERNAME: ${PACKHUB_DB_USER} 24 | MONGO_INITDB_ROOT_PASSWORD: ${PACKHUB_DB_PASSWORD} 25 | volumes: 26 | - mongodb_data:/data/db 27 | 28 | volumes: 29 | mongodb_data: 30 | -------------------------------------------------------------------------------- /images/debian12-multi-package.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:12 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENV DIST=debian 8 | 9 | ENTRYPOINT ["./check_apt_multiple.sh"] 10 | -------------------------------------------------------------------------------- /images/debian12.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:12 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENV DIST=debian 8 | 9 | ENTRYPOINT ["./check_apt.sh"] 10 | -------------------------------------------------------------------------------- /images/fedora38-multi-package.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:38 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENTRYPOINT ["./check_dnf_multiple.sh"] 8 | -------------------------------------------------------------------------------- /images/fedora38.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:38 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENTRYPOINT ["./check_dnf.sh"] 8 | -------------------------------------------------------------------------------- /images/server-ci.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN apt update && apt install -y clang llvm pkg-config nettle-dev 8 | 9 | RUN cargo build 10 | 11 | EXPOSE 3000 12 | 13 | ENTRYPOINT ["scripts/run_server.sh"] 14 | -------------------------------------------------------------------------------- /images/server.Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Compute the recipe file 2 | FROM lukemathwalker/cargo-chef:latest-rust-latest AS chef 3 | WORKDIR /app 4 | 5 | FROM chef AS planner 6 | COPY . . 7 | RUN cargo chef prepare --recipe-path recipe.json 8 | 9 | # Stage 2: Cache and Build the Rust binary 10 | FROM chef AS builder 11 | COPY --from=planner /app/recipe.json recipe.json 12 | 13 | RUN apt update && apt install -y clang llvm pkg-config nettle-dev 14 | 15 | # Build dependencies - this is the caching Docker layer! 16 | RUN cargo chef cook --release --recipe-path recipe.json 17 | # Build application 18 | COPY . . 19 | RUN cargo build --release 20 | 21 | # Stage 3: Minimal final runtime image 22 | FROM debian:bookworm-slim AS runtime 23 | 24 | RUN apt update && apt install -y libssl-dev ca-certificates clang llvm pkg-config nettle-dev 25 | RUN update-ca-certificates 26 | 27 | WORKDIR /app 28 | COPY --from=builder /app/target/release/packhub /app/packhub 29 | COPY /pages /app/pages 30 | 31 | EXPOSE 80 32 | EXPOSE 443 33 | ENTRYPOINT ["/app/packhub"] 34 | -------------------------------------------------------------------------------- /images/tumbleweed-multi-package.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM opensuse/tumbleweed:latest 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENTRYPOINT ["./check_zypper_multiple.sh"] 8 | -------------------------------------------------------------------------------- /images/ubuntu24.04-multi-package.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENV DIST=ubuntu 8 | 9 | ENTRYPOINT ["./check_apt_multiple.sh"] 10 | -------------------------------------------------------------------------------- /images/ubuntu24.04.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | ENV DIST=ubuntu 8 | 9 | ENTRYPOINT ["./check_apt.sh"] 10 | -------------------------------------------------------------------------------- /pages/assets/asciinema.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | # # Welcome to packhub! 24 | 25 | 26 | 27 | # # Let's install sindresorhus/caprine from Github through PackHub! 28 | 29 | 30 | 31 | # # At first, we'll need to execute a script to setup PackHub repoository in our system. 32 | 33 | 34 | 35 | # # For Ubuntu, the URL scheme is: 36 | 37 | 38 | 39 | # # wget -qO- https://packhub.dev/sh/ubuntu/github/OWNER/REPO | sh 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /pages/assets/debian.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages/assets/fedora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/pages/assets/fedora.png -------------------------------------------------------------------------------- /pages/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pages/assets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /pages/assets/openSUSE.svg: -------------------------------------------------------------------------------- 1 | 2 | openSUSE logo -------------------------------------------------------------------------------- /pages/assets/packhub.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base-color: #ffffff; 3 | --item-color: #f9f9f9; 4 | --item-text: #555555; 5 | --icon-color: #2A2A2A; 6 | --heading-color: #000000; 7 | --secondary-heading: #515151; 8 | } 9 | 10 | .darkmode { 11 | --base-color: #2A2A2A; 12 | --item-color: #333333; 13 | --item-text: #cccccc; 14 | --icon-color: #ffffff; 15 | --heading-color: #f0f0f0; 16 | --secondary-heading: #bbbbbb; 17 | } 18 | 19 | #theme-switch{ 20 | border: none; 21 | height: 50px; 22 | width: 50px; 23 | padding: 0; 24 | border-radius: 50%; 25 | background-color: var(--icon-color); 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | position: absolute; 30 | top: 25px; 31 | right: 25px; 32 | } 33 | #theme-switch svg{ 34 | fill: var(--base-color); 35 | } 36 | #theme-switch svg:last-child{ 37 | display: none; 38 | } 39 | .darkmode #theme-switch svg:first-child{ 40 | display: none; 41 | } 42 | .darkmode #theme-switch svg:last-child{ 43 | display: block; 44 | } 45 | 46 | body { 47 | margin-top: 2em; 48 | background-color: var(--base-color); 49 | color: var(--secondary-heading); 50 | font-family: "Fira Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 51 | font-weight: 300; 52 | font-size: 25px; 53 | } 54 | 55 | pre { 56 | font-family: "Fira Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, 57 | Consolas, "Liberation Mono", "Courier New", monospace; 58 | font-weight: 400; 59 | } 60 | 61 | .header-padding { 62 | padding-top: 1.5rem; 63 | } 64 | 65 | .text-code { 66 | font-family: "Fira Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, 67 | Consolas, "Liberation Mono", "Courier New", monospace; 68 | font-weight: 300; 69 | font-size: 1.4rem; 70 | } 71 | 72 | .text-bold { 73 | font-weight: 500; 74 | } 75 | 76 | .text-header { 77 | margin-top: 3rem; 78 | font-family: "Fira Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 79 | font-weight: 400; 80 | } 81 | 82 | .banner { 83 | display: flex; 84 | justify-content: space-between; 85 | align-items: center; 86 | } 87 | 88 | h1 { 89 | font-family: "Alfa Slab One", serif; 90 | color: var(--heading-color); 91 | font-size: 4rem; 92 | margin-bottom: 0; 93 | margin-top: 1rem; 94 | line-height: 1; 95 | font-weight: 300; 96 | letter-spacing: 1px; 97 | } 98 | 99 | h1 sup { 100 | font-family: "Fira Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 101 | font-size: 10px; 102 | color: #FFF; 103 | background-color: #0b7261; 104 | border-radius: 10px; 105 | padding: 7px; 106 | margin-left: 4px; 107 | margin-bottom: 50px; 108 | } 109 | 110 | 111 | h2 { 112 | color: var(--heading-color); 113 | font-size: 2rem; 114 | font-weight: 300; 115 | } 116 | 117 | h5 { 118 | color: var(--heading-color); 119 | font-size: 1.5rem; 120 | font-weight: 300; 121 | } 122 | 123 | .info p { 124 | margin-top: 0px; 125 | margin-bottom: 0px; 126 | font-size: 1rem; 127 | } 128 | 129 | a { 130 | color: #0b7261; 131 | text-decoration: underline; 132 | } 133 | 134 | a:hover { 135 | color: #0d8b75; 136 | text-decoration: underline; 137 | } 138 | 139 | main { 140 | width: 80%; 141 | max-width: 1200px; 142 | padding: 0 20px; 143 | margin-left: auto; 144 | margin-right: auto; 145 | } 146 | 147 | .instructions { 148 | margin-left: auto; 149 | margin-right: auto; 150 | margin-bottom: 2.5rem; 151 | text-align: center; 152 | border-radius: 8px; 153 | border: 1px solid rgb(204, 204, 204); 154 | box-shadow: 0px 1px 4px 0px rgb(204, 204, 204); 155 | padding-bottom: 2rem; 156 | } 157 | 158 | .instructions > * { 159 | margin-left: auto; 160 | margin-right: auto; 161 | } 162 | 163 | hr { 164 | border-color: #0b7261; 165 | margin-top: 2rem; 166 | margin-bottom: 3rem; 167 | } 168 | 169 | #card > p { 170 | width: 80%; 171 | } 172 | 173 | .video { 174 | margin-left: 3rem; 175 | margin-right: 3rem; 176 | } 177 | 178 | .platform-header { 179 | display: flex; 180 | align-items: center; 181 | justify-content: center; 182 | margin-top: 2rem; 183 | } 184 | 185 | .platform-header > span { 186 | padding-left: 1rem; 187 | } 188 | 189 | .command::before { 190 | color: black; 191 | content: " $ "; 192 | margin-left: 15px; 193 | } 194 | 195 | .command { 196 | color: black; 197 | padding: 1rem 1rem 1rem 0; 198 | height: auto; 199 | text-align: center; 200 | border-radius: 10px 0px 0px 10px; 201 | box-shadow: inset 0px 0px 20px 0px #f1eeee; 202 | background-color: #f1eeee; 203 | overflow: hidden; 204 | font-size: 0.6em; 205 | white-space: nowrap; 206 | height: 26px; 207 | line-height: 26px; 208 | overflow-x: auto; 209 | } 210 | 211 | .github-link { 212 | outline: none; 213 | border: none; 214 | display: inline-block; 215 | background-color: #f1eeee; 216 | border-radius: 10px; 217 | font-size: 0.6em; 218 | padding: 1rem 1rem 1rem 1rem; 219 | overflow: hidden; 220 | overflow-x: auto; 221 | width: 60%; 222 | margin-left: 50px; 223 | margin-right: 50px; 224 | text-align: center; 225 | } 226 | 227 | #card div.copy-container { 228 | display: flex; 229 | align-items: center; 230 | width: 90%; 231 | justify-content: center; 232 | } 233 | 234 | #card button.copy-button { 235 | height: 58px; 236 | margin: 0; 237 | padding: 0 5px 0 5px; 238 | border-radius: 0 10px 10px 0; 239 | border-left-width: 0px; 240 | border-left-style: solid; 241 | border: none; 242 | background-color: #d0d2d2; 243 | 244 | &:hover { 245 | background-color: #c0c2c2; 246 | } 247 | } 248 | 249 | #card div.copy-icon { 250 | position: relative; 251 | width: fit-content; 252 | height: fit-content; 253 | top: 27px; 254 | left: 50%; 255 | transform: translate(-50%, -50%); 256 | } 257 | 258 | #card div.copy-button-text { 259 | transition: opacity 0.2s ease-in-out; 260 | opacity: 0; 261 | font-size: 10px; 262 | color: #0b7261; 263 | width: 41px; 264 | height: 15px; 265 | position: relative; 266 | top: 5px; 267 | } 268 | 269 | #about { 270 | font-size: 16px; 271 | line-height: 2em; 272 | text-align: center; 273 | } 274 | 275 | .features-grid { 276 | display: grid; 277 | grid-template-columns: repeat(2, 1fr); 278 | gap: 20px; 279 | margin: 40px 0; 280 | } 281 | 282 | .feature-item { 283 | background-color: var(--item-color); 284 | border: 1px solid #ddd; 285 | border-radius: 8px; 286 | padding: 20px; 287 | text-align: center; 288 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 289 | } 290 | 291 | .feature-item svg path { 292 | fill: var(--icon-color); 293 | } 294 | 295 | .feature-item h3 { 296 | margin-top: 10px; 297 | margin-bottom: 10px; 298 | font-size: 1.5rem; 299 | } 300 | 301 | .feature-item p { 302 | font-size: 1rem; 303 | color: var(--item-text); 304 | } 305 | 306 | @media (max-width: 768px) { 307 | .features-grid { 308 | grid-template-columns: 1fr; /* Switch to a single column */ 309 | } 310 | 311 | header h2 { 312 | text-align: center; 313 | } 314 | 315 | header h5 { 316 | text-align: justify; 317 | } 318 | } 319 | 320 | @media (max-width: 576px) { 321 | .banner { 322 | flex-direction: column; 323 | align-items: center; 324 | gap: 1.5rem; 325 | } 326 | 327 | .banner h1 { 328 | font-size: 3rem; 329 | } 330 | 331 | header h2 { 332 | text-align: center; 333 | } 334 | 335 | header h5 { 336 | text-align: justify; 337 | } 338 | 339 | .github-link { 340 | width: 80%; 341 | margin-left: 10px; 342 | margin-right: 10px; 343 | } 344 | 345 | .platform-header { 346 | flex-direction: column; 347 | align-items: center; 348 | gap: 0.5rem; 349 | } 350 | 351 | .platform-header > span { 352 | padding-left: 0px; 353 | } 354 | 355 | .about-contrib { 356 | display: block; 357 | } 358 | 359 | h2.text-header { 360 | font-size: 1.5rem; 361 | } 362 | 363 | .info p { 364 | font-size: 0.8rem; 365 | } 366 | 367 | p#about { 368 | font-size: 0.9rem; 369 | } 370 | } -------------------------------------------------------------------------------- /pages/assets/packhub.js: -------------------------------------------------------------------------------- 1 | let darkmode = localStorage.getItem('darkmode') 2 | const themeSwitch = document.getElementById('theme-switch') 3 | 4 | const enableDarkmode = () => { 5 | document.body.classList.add('darkmode') 6 | localStorage.setItem('darkmode', 'active') 7 | } 8 | 9 | const disableDarkmode = () => { 10 | document.body.classList.remove('darkmode') 11 | localStorage.setItem('darkmode', null) 12 | } 13 | 14 | if(darkmode === "active") enableDarkmode() 15 | 16 | themeSwitch.addEventListener("click", () => { 17 | darkmode = localStorage.getItem('darkmode') 18 | darkmode !== "active" ? enableDarkmode() : disableDarkmode() 19 | }) 20 | 21 | function process_copy_button_click(id, klass) { 22 | try { 23 | const command = document.querySelector(`.command.${klass}`); 24 | navigator.clipboard.writeText(command.textContent).then(() => 25 | document.getElementById(id).style.opacity = '1'); 26 | 27 | setTimeout(() => document.getElementById(id).style.opacity = '0', 3000); 28 | } catch (e) { 29 | console.log('Hit a snag when copying to clipboard: ', e); 30 | } 31 | } 32 | 33 | function handle_copy_button_click(e) { 34 | switch (e.id) { 35 | case 'copy-button-ubuntu': 36 | process_copy_button_click('copy-status-message-ubuntu', 'ubuntu'); 37 | break; 38 | case 'copy-button-debian': 39 | process_copy_button_click('copy-status-message-debian', 'debian'); 40 | break; 41 | case 'copy-button-fedora': 42 | process_copy_button_click('copy-status-message-fedora', 'fedora'); 43 | break; 44 | case 'copy-button-suse': 45 | process_copy_button_click('copy-status-message-suse', 'suse'); 46 | break; 47 | } 48 | } 49 | 50 | function set_up_copy_button_clicks() { 51 | var buttons = document.querySelectorAll(".copy-button"); 52 | buttons.forEach(function (element) { 53 | element.addEventListener('click', function() { 54 | handle_copy_button_click(element); 55 | }); 56 | }) 57 | } 58 | 59 | set_up_copy_button_clicks(); 60 | 61 | function set_ubuntu(owner, repo) { 62 | const ubuntu = document.querySelector(".command.ubuntu"); 63 | 64 | console.log("Setting Ubuntu install command for:", ubuntu); 65 | 66 | ubuntu.textContent = `wget -qO- https://packhub.dev/sh/ubuntu/github/${owner}/${repo} | sh` 67 | } 68 | 69 | function set_debian(owner, repo) { 70 | const ubuntu = document.querySelector(".command.debian"); 71 | 72 | console.log("Setting Ubuntu install command for:", ubuntu); 73 | 74 | ubuntu.textContent = `wget -qO- https://packhub.dev/sh/debian/github/${owner}/${repo} | sh` 75 | } 76 | 77 | function set_fedora(owner, repo) { 78 | const rpm = document.querySelector(".command.fedora"); 79 | 80 | console.log("Setting Fedora install command for:", rpm); 81 | 82 | rpm.textContent = `wget -qO- https://packhub.dev/sh/yum/github/${owner}/${repo} | sh` 83 | } 84 | 85 | function set_suse(owner, repo) { 86 | const rpm = document.querySelector(".command.suse"); 87 | 88 | console.log("Setting Suse install command for:", rpm); 89 | 90 | rpm.textContent = `wget -qO- https://packhub.dev/sh/zypp/github/${owner}/${repo} | sh` 91 | } 92 | 93 | document.addEventListener("DOMContentLoaded", () => { 94 | const inputElement = document.querySelector(".github-link"); 95 | 96 | function extractGithubInfo(value) { 97 | const githubRegex = /https?:\/\/github\.com\/([^\/]+)\/([^\/]+)/; 98 | const match = value.match(githubRegex); 99 | 100 | if (match) { 101 | set_ubuntu(match[1], match[2]); 102 | set_debian(match[1], match[2]); 103 | set_fedora(match[1], match[2]); 104 | set_suse(match[1], match[2]); 105 | } else { 106 | console.log("Invalid or missing GitHub URL"); 107 | } 108 | } 109 | 110 | if (inputElement) { 111 | // Extract initial value (if present) 112 | extractGithubInfo(inputElement.value || inputElement.placeholder); 113 | 114 | // Listen for changes in the input field 115 | inputElement.addEventListener("input", (event) => { 116 | extractGithubInfo(inputElement.value || inputElement.placeholder); 117 | }); 118 | } 119 | }); 120 | 121 | -------------------------------------------------------------------------------- /pages/assets/rpm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pages/assets/ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mominul/packhub/b4c18fe7316d292734370fe302baa8f4f83a5545/pages/assets/ubuntu.png -------------------------------------------------------------------------------- /scripts/check_apt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apt update 4 | apt install sudo wget -y 5 | 6 | echo 7 | echo "Running the package key and repository setup script" 8 | 9 | wget -qO- http://localhost:3000/sh/$DIST/github/mominul/pack-exp3 | sh 10 | return_value=$? 11 | 12 | if [ $return_value -ne 0 ]; then 13 | echo "The script failed with exit code $return_value" 14 | # Handle error case here 15 | exit $return_value 16 | else 17 | echo "Package key and repository setup script ran successfully." 18 | fi 19 | 20 | output=$(apt search openbangla 2>&1) 21 | status=$? 22 | 23 | # Print the output of the apt command 24 | echo "$output" 25 | 26 | # Check if the apt command was successful 27 | if [ $status -ne 0 ]; then 28 | echo "Error: apt search command failed." >&2 29 | exit $status 30 | fi 31 | 32 | if echo "$output" | grep -q "openbangla-keyboard"; then 33 | echo 34 | echo "Package found successfully." 35 | else 36 | echo "Error: package not found." >&2 37 | exit 1 38 | fi 39 | 40 | # check if apt can install the package 41 | apt_out=$(apt install openbangla-keyboard -y 2>&1) 42 | apt_status=$? 43 | 44 | # Print the output of the apt command 45 | echo "$apt_out" 46 | 47 | # Check if the apt command was successful 48 | if [ $apt_status -ne 0 ]; then 49 | echo "Error: apt install command failed." >&2 50 | exit $apt_status 51 | fi 52 | 53 | echo "Package installed successfully." 54 | exit 0 55 | -------------------------------------------------------------------------------- /scripts/check_apt_multiple.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apt update 4 | apt install sudo wget -y 5 | 6 | wget -qO- http://localhost:3000/sh/$DIST/github/mominul/pack-exp2 | sh 7 | 8 | output=$(apt search openbangla 2>&1) 9 | status=$? 10 | 11 | # Print the output of the apt command 12 | echo "$output" 13 | 14 | # Check if the apt command was successful 15 | if [ $status -ne 0 ]; then 16 | echo "Error: apt search command failed." >&2 17 | exit $status 18 | fi 19 | 20 | # Check if `fcitx-openbangla` is in the output 21 | if echo "$output" | grep -q "fcitx-openbangla"; then 22 | echo 23 | echo "Package fcitx-openbangla found." 24 | else 25 | echo "Error: fcitx-openbangla not found." >&2 26 | exit 1 27 | fi 28 | 29 | # Check if `ibus-openbangla` is in the output 30 | if echo "$output" | grep -q "ibus-openbangla"; then 31 | echo 32 | echo "Package ibus-openbangla found." 33 | else 34 | echo "Error: ibus-openbangla not found." >&2 35 | exit 1 36 | fi 37 | -------------------------------------------------------------------------------- /scripts/check_dnf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yes | dnf install wget sudo 4 | 5 | echo 6 | echo "Running the package key and repository setup script" 7 | 8 | wget -qO- http://localhost:3000/sh/yum/github/mominul/pack-exp3 | sh 9 | return_value=$? 10 | 11 | if [ $return_value -ne 0 ]; then 12 | echo "The script failed with exit code $return_value" 13 | # Handle error case here 14 | exit $return_value 15 | else 16 | echo "Package key and repository setup script ran successfully." 17 | fi 18 | 19 | output=$(yes | dnf search openbangla 2>&1) 20 | status=$? 21 | 22 | # Print the output of the dnf command 23 | echo "$output" 24 | 25 | # Check if the dnf command was successful 26 | if [ $status -ne 0 ]; then 27 | echo "Error: dnf search command failed." >&2 28 | exit $status 29 | fi 30 | 31 | if echo "$output" | grep -q "openbangla-keyboard"; then 32 | echo 33 | echo "Package found successfully." 34 | else 35 | echo "Error: package not found." >&2 36 | exit 1 37 | fi 38 | 39 | # check if dnf can install the package 40 | dnf_out=$(yes | dnf install openbangla-keyboard 2>&1) 41 | dnf_status=$? 42 | 43 | # Print the output of the dnf command 44 | echo "$dnf_out" 45 | 46 | # Check if the dnf command was successful 47 | if [ $dnf_status -ne 0 ]; then 48 | echo "Error: dnf install command failed." >&2 49 | exit $dnf_status 50 | fi 51 | 52 | echo "Package installed successfully." 53 | exit 0 54 | -------------------------------------------------------------------------------- /scripts/check_dnf_multiple.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yes | dnf install wget sudo 4 | 5 | echo 6 | echo "Running the package key and repository setup script" 7 | 8 | wget -qO- http://localhost:3000/sh/yum/github/mominul/pack-exp2 | sh 9 | return_value=$? 10 | if [ $return_value -ne 0 ]; then 11 | echo "The script failed with exit code $return_value" 12 | # Handle error case here 13 | exit $return_value 14 | else 15 | echo "Package key and repository setup script ran successfully." 16 | fi 17 | 18 | output=$(yes | dnf search openbangla 2>&1) 19 | status=$? 20 | 21 | # Print the output of the dnf command 22 | echo "$output" 23 | 24 | # Check if the dnf command was successful 25 | if [ $status -ne 0 ]; then 26 | echo "Error: dnf search command failed." >&2 27 | exit $status 28 | fi 29 | 30 | # Check if `fcitx-openbangla` is in the output 31 | if echo "$output" | grep -q "fcitx-openbangla"; then 32 | echo 33 | echo "Package fcitx-openbangla found." 34 | else 35 | echo "Error: fcitx-openbangla not found." >&2 36 | exit 1 37 | fi 38 | 39 | # Check if `ibus-openbangla` is in the output 40 | if echo "$output" | grep -q "ibus-openbangla"; then 41 | echo 42 | echo "Package ibus-openbangla found." 43 | else 44 | echo "Error: ibus-openbangla not found." >&2 45 | exit 1 46 | fi 47 | -------------------------------------------------------------------------------- /scripts/check_zypper_multiple.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | zypper --non-interactive install wget sudo 4 | 5 | echo 6 | echo "Running the package key and repository setup script" 7 | 8 | wget -qO- http://localhost:3000/sh/zypp/github/mominul/pack-exp2 | sh 9 | return_value=$? 10 | if [ $return_value -ne 0 ]; then 11 | echo "The script failed with exit code $return_value" 12 | # Handle error case here 13 | exit $return_value 14 | else 15 | echo "Package key and repository setup script ran successfully." 16 | fi 17 | 18 | zypper --gpg-auto-import-keys refresh 19 | 20 | output=$(zypper search openbangla 2>&1) 21 | status=$? 22 | 23 | # Print the output of the zypper command 24 | echo "$output" 25 | 26 | # Check if the dnf command was successful 27 | if [ $status -ne 0 ]; then 28 | echo "Error: zypper search command failed." >&2 29 | exit $status 30 | fi 31 | 32 | # Check if `fcitx-openbangla` is in the output 33 | if echo "$output" | grep -q "fcitx-openbangla"; then 34 | echo 35 | echo "Package fcitx-openbangla found." 36 | else 37 | echo "Error: fcitx-openbangla not found." >&2 38 | exit 1 39 | fi 40 | 41 | # Check if `ibus-openbangla` is in the output 42 | if echo "$output" | grep -q "ibus-openbangla"; then 43 | echo 44 | echo "Package ibus-openbangla found." 45 | else 46 | echo "Error: ibus-openbangla not found." >&2 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /scripts/deploy.ps1: -------------------------------------------------------------------------------- 1 | docker build -t packhub:latest -f images/server.Dockerfile . 2 | docker tag packhub:latest mominul/packhub:latest 3 | docker push mominul/packhub:latest 4 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build --platform linux/amd64 -t packhub:latest -f images/server.Dockerfile . 4 | docker tag packhub:latest mominul/packhub:latest 5 | docker push mominul/packhub:latest 6 | -------------------------------------------------------------------------------- /scripts/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./target/debug/packhub --generate-keys -------------------------------------------------------------------------------- /src/apt/deb.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::sync::LazyLock; 3 | 4 | use anyhow::{Context, Result, bail}; 5 | use libflate::gzip::Decoder; 6 | use md5::Md5; 7 | use regex::Regex; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json::{from_str, to_string}; 10 | use sha1::Sha1; 11 | use sha2::{Sha256, Sha512}; 12 | 13 | use crate::{ 14 | package::{Data, Package}, 15 | utils::{Arch, hashsum}, 16 | }; 17 | 18 | static ARCH: LazyLock = LazyLock::new(|| Regex::new(r#"Architecture: (\w+)"#).unwrap()); 19 | 20 | /// Debian package (.deb) 21 | #[derive(Serialize, Deserialize, Debug)] 22 | pub struct DebianPackage { 23 | pub control: String, 24 | pub md5: String, 25 | pub sha1: String, 26 | pub sha256: String, 27 | pub sha512: String, 28 | pub size: usize, 29 | pub filename: String, 30 | } 31 | 32 | impl DebianPackage { 33 | /// Create a new Debian package from a package. 34 | /// 35 | /// Also sets metadata of the package. 36 | pub fn from_package(package: &Package) -> Result { 37 | // Create the debian package from the metadata if it is present. 38 | if let Data::Metadata(metadata) = package.data() { 39 | let package: DebianPackage = from_str(&metadata)?; 40 | 41 | return Ok(package); 42 | } 43 | 44 | let Data::Package(data) = package.data() else { 45 | bail!("Package data is not available"); 46 | }; 47 | 48 | let control = read_control_file(&data) 49 | .context("Error occurred while parsing the debian control file from package")? 50 | .trim_end() 51 | .to_owned(); 52 | let filename = format!("pool/stable/{}/{}", package.version(), package.file_name()); 53 | 54 | let size = data.len(); 55 | let md5 = hashsum::(&data); 56 | let sha1 = hashsum::(&data); 57 | let sha256 = hashsum::(&data); 58 | let sha512 = hashsum::(&data); 59 | 60 | let deb = Self { 61 | control, 62 | md5, 63 | sha1, 64 | sha256, 65 | sha512, 66 | size, 67 | filename, 68 | }; 69 | 70 | let metadata = to_string(&deb)?; 71 | package.set_metadata(metadata); 72 | 73 | Ok(deb) 74 | } 75 | 76 | /// Get the architecture for which the package is built for. 77 | pub fn get_arch(&self) -> Option { 78 | ARCH.captures(&self.control) 79 | .unwrap() 80 | .get(1) 81 | .unwrap() 82 | .as_str() 83 | .parse() 84 | .ok() 85 | } 86 | } 87 | 88 | fn read_control_file(data: &[u8]) -> Result { 89 | let mut archive = ar::Archive::new(data); 90 | 91 | while let Some(entry_result) = archive.next_entry() { 92 | let mut entry = entry_result?; 93 | let header = entry.header(); 94 | let name = String::from_utf8_lossy(header.identifier()); 95 | if name == "control.tar.gz" { 96 | let mut data = Vec::new(); 97 | entry.read_to_end(&mut data)?; 98 | 99 | // Un-compress the control.tar.gz 100 | let mut decoder = Decoder::new(&data[..])?; 101 | let mut data = Vec::new(); 102 | decoder.read_to_end(&mut data)?; 103 | 104 | // Read the control.tar archive 105 | let mut archive = tar::Archive::new(&data[..]); 106 | for entry in archive.entries()? { 107 | let mut entry = entry?; 108 | 109 | let path = entry.path()?; 110 | let path = path.to_str().unwrap(); 111 | 112 | if path == "./control" { 113 | let mut control = String::new(); 114 | entry.read_to_string(&mut control)?; 115 | 116 | return Ok(control); 117 | } 118 | } 119 | } 120 | } 121 | 122 | bail!("Control file not found"); 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use std::fs::read; 128 | 129 | use super::*; 130 | use crate::package::tests::package; 131 | 132 | #[test] 133 | fn test_parsing() { 134 | let package = package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb"); 135 | let data = read("data/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb").unwrap(); 136 | package.set_package_data(data); 137 | 138 | let deb = DebianPackage::from_package(&package).unwrap(); 139 | assert_eq!(deb.get_arch(), Some(Arch::Amd64)); 140 | } 141 | 142 | #[test] 143 | #[should_panic] 144 | fn test_without_data() { 145 | let package = package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb"); 146 | 147 | let _ = DebianPackage::from_package(&package).unwrap(); 148 | } 149 | 150 | #[test] 151 | fn test_loading_from_metadata() { 152 | let package = package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb"); 153 | let data = read("data/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb").unwrap(); 154 | package.set_package_data(data); 155 | 156 | let _ = DebianPackage::from_package(&package).unwrap(); 157 | 158 | // The package data should have been replaced by the metadata 159 | assert!(matches!(package.data(), Data::Metadata(_))); 160 | 161 | let _ = DebianPackage::from_package(&package).unwrap(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/apt/index.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, io::Write}; 2 | 3 | use anyhow::Result; 4 | use askama::Template; 5 | use chrono::{DateTime, Utc}; 6 | use libflate::gzip::{EncodeOptions, Encoder, HeaderBuilder}; 7 | use md5::Md5; 8 | use sha1::Sha1; 9 | use sha2::{Sha256, Sha512}; 10 | 11 | use crate::{ 12 | apt::deb::DebianPackage, 13 | package::Package, 14 | utils::{Arch, hashsum}, 15 | }; 16 | 17 | #[derive(Debug)] 18 | pub struct AptIndices { 19 | packages: HashMap>, 20 | date: DateTime, 21 | } 22 | 23 | #[derive(Template)] 24 | #[template(path = "Release")] 25 | struct ReleaseIndex<'a> { 26 | origin: &'a str, 27 | label: &'a str, 28 | date: String, 29 | files: Vec, 30 | } 31 | 32 | #[derive(Template)] 33 | #[template(path = "Packages")] 34 | struct PackageIndex<'a> { 35 | packages: &'a [DebianPackage], 36 | } 37 | 38 | struct Files { 39 | md5: String, 40 | sha1: String, 41 | sha256: String, 42 | sha512: String, 43 | size: usize, 44 | path: String, 45 | } 46 | 47 | impl AptIndices { 48 | pub fn new(packages: &[Package]) -> Result { 49 | let mut debian: HashMap> = HashMap::new(); 50 | // Find the latest date from the list of packages 51 | let mut date = DateTime::UNIX_EPOCH; 52 | for package in packages { 53 | if *package.creation_date() > date { 54 | date = *package.creation_date(); 55 | } 56 | 57 | match DebianPackage::from_package(package) { 58 | Ok(deb) => { 59 | if let Some(arch) = deb.get_arch() { 60 | debian.entry(arch).or_default().push(deb); 61 | } else { 62 | tracing::error!( 63 | "Debian package architecture not found for package: {:?}", 64 | package 65 | ); 66 | continue; 67 | } 68 | } 69 | Err(e) => { 70 | tracing::error!("Error occurred when extracting debian control data: {e}"); 71 | continue; 72 | } 73 | } 74 | } 75 | Ok(AptIndices { 76 | packages: debian, 77 | date, 78 | }) 79 | } 80 | 81 | pub fn get_package_index(&self, arch: &Arch) -> String { 82 | let index = PackageIndex { 83 | packages: self.packages.get(arch).unwrap(), 84 | }; 85 | index.render().unwrap().trim().to_owned() 86 | } 87 | 88 | pub fn get_release_index(&self) -> String { 89 | let name = ". stable"; 90 | let date = self.date.to_rfc2822(); 91 | 92 | let mut files = vec![]; 93 | 94 | for arch in self.packages.keys() { 95 | let packages = self.get_package_index(arch); 96 | let packages = packages.as_bytes(); 97 | let packages_gz = gzip_compression(packages); 98 | 99 | files.extend([ 100 | Files { 101 | sha256: hashsum::(packages), 102 | size: packages.len(), 103 | path: format!("main/binary-{}/Packages", arch), 104 | md5: hashsum::(packages), 105 | sha1: hashsum::(packages), 106 | sha512: hashsum::(packages), 107 | }, 108 | Files { 109 | sha256: hashsum::(&packages_gz), 110 | size: packages_gz.len(), 111 | path: format!("main/binary-{}/Packages.gz", arch), 112 | md5: hashsum::(&packages_gz), 113 | sha1: hashsum::(&packages_gz), 114 | sha512: hashsum::(&packages_gz), 115 | }, 116 | ]); 117 | } 118 | 119 | // Sort the files 120 | files.sort_by(|a, b| a.path.cmp(&b.path)); 121 | 122 | let index = ReleaseIndex { 123 | date, 124 | files, 125 | origin: name, 126 | label: name, 127 | }; 128 | 129 | index.render().unwrap() 130 | } 131 | } 132 | 133 | pub fn gzip_compression(data: &[u8]) -> Vec { 134 | let header = HeaderBuilder::new().modification_time(0).finish(); 135 | let options = EncodeOptions::new().header(header); 136 | let mut encoder = Encoder::with_options(Vec::new(), options).unwrap(); 137 | encoder.write_all(data).unwrap(); 138 | 139 | let gzip = encoder.finish(); 140 | 141 | gzip.into_result().unwrap() 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use std::fs::{self, read}; 147 | 148 | use chrono::DateTime; 149 | use insta::assert_snapshot; 150 | 151 | use super::*; 152 | use crate::package::tests::package_with_ver; 153 | 154 | #[test] 155 | fn test_apt_indices() { 156 | let package = Package::detect_package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb", "2.0.0".to_owned(), "https://github.com/OpenBangla/OpenBangla-Keyboard/releases/download/2.0.0/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb".to_owned(), DateTime::parse_from_rfc2822("Wed, 8 Nov 2023 16:40:12 +0000").unwrap().into()).unwrap(); 157 | let data = read("data/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb").unwrap(); 158 | package.set_package_data(data); 159 | 160 | let packages = vec![package]; 161 | 162 | let indices = AptIndices::new(&packages).unwrap(); 163 | 164 | // Packages 165 | let packages = indices.get_package_index(&Arch::Amd64); 166 | assert_snapshot!(packages); 167 | 168 | // Release 169 | let release = indices.get_release_index(); 170 | assert_snapshot!(release); 171 | } 172 | 173 | #[test] 174 | fn test_multiple_packages() { 175 | let package1 = package_with_ver("fcitx-openbangla_3.0.0.deb", "3.0.0"); 176 | let data = fs::read("data/fcitx-openbangla_3.0.0.deb").unwrap(); 177 | package1.set_package_data(data); 178 | 179 | let package2 = package_with_ver("ibus-openbangla_3.0.0.deb", "3.0.0"); 180 | let data = fs::read("data/ibus-openbangla_3.0.0.deb").unwrap(); 181 | package2.set_package_data(data); 182 | 183 | let packages = vec![package1, package2]; 184 | 185 | let indices = AptIndices::new(&packages).unwrap(); 186 | 187 | // Packages 188 | let packages = indices.get_package_index(&Arch::Amd64); 189 | assert_snapshot!(packages); 190 | assert_eq!(packages.as_bytes().len(), 2729); 191 | let packages_gz = gzip_compression(packages.as_bytes()); 192 | assert_eq!(packages_gz.len(), 1105); 193 | 194 | // Release 195 | let release = indices.get_release_index(); 196 | assert_snapshot!(release); 197 | } 198 | 199 | #[test] 200 | fn test_multiple_architectures() { 201 | let package1 = package_with_ver("fastfetch-linux-aarch64.deb", "2.40.3"); 202 | let data = fs::read("data/fastfetch-linux-aarch64.deb").unwrap(); 203 | package1.set_package_data(data); 204 | 205 | let package2 = package_with_ver("fastfetch-linux-amd64.deb", "2.40.3"); 206 | let data = fs::read("data/fastfetch-linux-amd64.deb").unwrap(); 207 | package2.set_package_data(data); 208 | 209 | let package3 = package_with_ver("fastfetch-linux-armv6l.deb", "2.40.3"); 210 | let data = fs::read("data/fastfetch-linux-armv6l.deb").unwrap(); 211 | package3.set_package_data(data); 212 | 213 | let package4 = package_with_ver("fastfetch-linux-armv7l.deb", "2.40.3"); 214 | let data = fs::read("data/fastfetch-linux-armv7l.deb").unwrap(); 215 | package4.set_package_data(data); 216 | 217 | let package5 = package_with_ver("fastfetch-linux-ppc64le.deb", "2.40.3"); 218 | let data = fs::read("data/fastfetch-linux-ppc64le.deb").unwrap(); 219 | package5.set_package_data(data); 220 | 221 | let package6 = package_with_ver("fastfetch-linux-riscv64.deb", "2.40.3"); 222 | let data = fs::read("data/fastfetch-linux-riscv64.deb").unwrap(); 223 | package6.set_package_data(data); 224 | 225 | let package7 = package_with_ver("fastfetch-linux-s390x.deb", "2.40.3"); 226 | let data = fs::read("data/fastfetch-linux-s390x.deb").unwrap(); 227 | package7.set_package_data(data); 228 | 229 | let packages = [ 230 | package1, package2, package3, package4, package5, package6, package7, 231 | ]; 232 | 233 | let indices = AptIndices::new(&packages).unwrap(); 234 | 235 | // Release 236 | let release = indices.get_release_index(); 237 | assert_snapshot!(release); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/apt/mod.rs: -------------------------------------------------------------------------------- 1 | mod deb; 2 | mod index; 3 | mod routes; 4 | 5 | pub use self::routes::apt_routes; 6 | #[cfg(test)] 7 | pub use deb::DebianPackage; 8 | -------------------------------------------------------------------------------- /src/apt/routes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, anyhow}; 2 | use axum::{ 3 | Router, 4 | body::Body, 5 | extract::{Path, State}, 6 | response::IntoResponse, 7 | routing::get, 8 | }; 9 | use axum_extra::{headers::UserAgent, typed_header::TypedHeader}; 10 | 11 | use crate::{ 12 | REQWEST, 13 | apt::index::{AptIndices, gzip_compression}, 14 | error::AppError, 15 | repository::Repository, 16 | state::AppState, 17 | utils::Arch, 18 | }; 19 | 20 | #[tracing::instrument(name = "Debian Release File", skip_all, fields(agent = agent.as_str()))] 21 | async fn release_index( 22 | State(state): State, 23 | Path((distro, owner, repo, file)): Path<(String, String, String, String)>, 24 | TypedHeader(agent): TypedHeader, 25 | ) -> Result, AppError> { 26 | let mut repo = Repository::from_github(owner, repo, &state).await; 27 | let packages = repo.select_package_apt(&distro, agent.as_str()).await?; 28 | 29 | let index = AptIndices::new(&packages)?; 30 | repo.save_package_metadata().await; 31 | 32 | let release_file = index.get_release_index(); 33 | 34 | match file.as_str() { 35 | "Release" => Ok(release_file.into_bytes()), 36 | "Release.gpg" => { 37 | let signed_release_file = state.detached_sign_metadata(&release_file)?; 38 | Ok(signed_release_file) 39 | } 40 | "InRelease" => { 41 | let signed_release_file = state.clearsign_metadata(&release_file)?; 42 | Ok(signed_release_file) 43 | } 44 | file => Err(anyhow!("Unknown file requested: {file}").into()), 45 | } 46 | } 47 | 48 | #[tracing::instrument(name = "Debian Package metadata file", skip_all, fields(agent = agent.as_str()))] 49 | async fn packages_file( 50 | State(state): State, 51 | Path((distro, owner, repo, arch, file)): Path<(String, String, String, String, String)>, 52 | TypedHeader(agent): TypedHeader, 53 | ) -> Result, AppError> { 54 | let mut repo = Repository::from_github(owner, repo, &state).await; 55 | let packages = repo.select_package_apt(&distro, agent.as_str()).await?; 56 | 57 | let index = AptIndices::new(&packages)?; 58 | repo.save_package_metadata().await; 59 | 60 | let Ok(arch) = arch.parse::() else { 61 | return Err(anyhow!("Unknown architecture: {arch}").into()); 62 | }; 63 | 64 | match file.as_str() { 65 | "Packages" => Ok(index.get_package_index(&arch).as_bytes().to_owned()), 66 | "Packages.gz" => Ok(gzip_compression(index.get_package_index(&arch).as_bytes())), 67 | file => Err(anyhow!("Unknown file requested: {file}").into()), 68 | } 69 | } 70 | 71 | async fn empty_packages_file( 72 | Path((_, _, _, file)): Path<(String, String, String, String)>, 73 | ) -> Result, AppError> { 74 | match file.as_str() { 75 | "Packages" => Ok(Vec::new()), 76 | "Packages.gz" => Ok(gzip_compression(&Vec::new())), 77 | file => Err(anyhow!("Unknown file requested: {file}").into()), 78 | } 79 | } 80 | 81 | #[tracing::instrument(name = "Debian Package proxy", skip_all)] 82 | async fn pool( 83 | Path((_, owner, repo, ver, file)): Path<(String, String, String, String, String)>, 84 | ) -> Result { 85 | let url = format!("https://github.com/{owner}/{repo}/releases/download/{ver}/{file}"); 86 | tracing::trace!("Proxying package from: {}", url); 87 | let res = REQWEST 88 | .get(url) 89 | .send() 90 | .await 91 | .context("Error occurred while proxying package")?; 92 | tracing::trace!("Proxying package respone: {}", res.status()); 93 | let stream = Body::from_stream(res.bytes_stream()); 94 | 95 | Ok(stream) 96 | } 97 | 98 | pub fn apt_routes() -> Router { 99 | Router::new() 100 | .route( 101 | "/{distro}/github/{owner}/{repo}/dists/stable/{file}", 102 | get(release_index), 103 | ) 104 | .route( 105 | "/{distro}/github/{owner}/{repo}/dists/stable/main/binary-{arch}/{index}", 106 | get(packages_file), 107 | ) 108 | .route( 109 | "/{distro}/github/{owner}/{repo}/dists/stable/main/binary-all/{index}", 110 | get(empty_packages_file), 111 | ) 112 | .route( 113 | "/{distro}/github/{owner}/{repo}/pool/stable/{ver}/{file}", 114 | get(pool), 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/apt/snapshots/packhub__apt__index__tests__apt_indices-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apt/index.rs 3 | expression: release 4 | --- 5 | Origin: . stable 6 | Label: . stable 7 | Suite: stable 8 | Codename: stable 9 | Date: Wed, 8 Nov 2023 16:40:12 +0000 10 | Architectures: amd64 11 | Components: main 12 | Description: Generated by packhub 13 | MD5Sum: 14 | a7a899656b17057ca8d11f72795f7632 1368 main/binary-amd64/Packages 15 | 3db94630abad4f6a59aed3f3095436e4 825 main/binary-amd64/Packages.gz 16 | SHA1: 17 | 14fb46db1939a078fb86abd659b1119dd67e617c 1368 main/binary-amd64/Packages 18 | 891e262000230983e733d4631ba0eb9859945cc7 825 main/binary-amd64/Packages.gz 19 | SHA256: 20 | 6eef82adbd7d262f9d94941e99f046604a1681c1f9f3a6f4cc0792c0ce5ce713 1368 main/binary-amd64/Packages 21 | 4777849b61148c8825c43b0c71c1b30ed782043ba2840ea0915f5af8a2ee65a0 825 main/binary-amd64/Packages.gz 22 | SHA512: 23 | 24b8d6278a63025fa2e9ed0b2ca1afbeb5d8f3fa2c4737d77b258d24dc30618c3cc6a3957944130c4ec04c42be455710cc1643d1e7d71caeedb7242ee996efb7 1368 main/binary-amd64/Packages 24 | 181a587e11e6ecc364dd53fa9b76cc47d647478988b1d08b5ed4fa77957dfcdbff9e85ff395f6b0ece9a922404283598e996adc78a16d646c08808cb811290d8 825 main/binary-amd64/Packages.gz 25 | -------------------------------------------------------------------------------- /src/apt/snapshots/packhub__apt__index__tests__apt_indices.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apt/index.rs 3 | expression: packages 4 | --- 5 | Architecture: amd64 6 | Depends: ibus (>= 1.5.1), libc6 (>= 2.29), libgcc-s1 (>= 4.2), libglib2.0-0 (>= 2.12.0), libibus-1.0-5 (>= 1.5.1), libqt5core5a (>= 5.12.2), libqt5gui5 (>= 5.0.2) | libqt5gui5-gles (>= 5.0.2), libqt5network5 (>= 5.0.2), libqt5widgets5 (>= 5.0.2), libstdc++6 (>= 5.2), libzstd1 (>= 1.3.2) 7 | Description: OpenSource Bengali input method 8 | OpenBangla Keyboard is an OpenSource, Unicode compliant Bengali Input Method for GNU/Linux systems. It's a full fledged Bengali input method with typing automation tools, includes many famous typing methods such as Avro Phonetic, Probhat, Munir Optima, National (Jatiya) etc. 9 | . 10 | Most of the features of Avro Keyboard are present in OpenBangla Keyboard. So Avro Keyboard users will feel at home with OpenBangla Keyboard in Linux. 11 | . 12 | Homepage: https://openbangla.github.io/ 13 | Maintainer: OpenBangla Team 14 | Package: openbangla-keyboard 15 | Priority: optional 16 | Section: utils 17 | Version: 2.0.0 18 | Installed-Size: 12263 19 | MD5sum: 3b026000480fda79cfd30976f569a820 20 | SHA1: 38e8fa44c049460169bf0b892724f9f1e50d4e50 21 | SHA256: d368f56f14d07d0f32482d9e56e68750ed1b4f68157fc53f6574c6fde6c4457e 22 | SHA512: b096eb4d8090533f77615e982a01421181680d25a9d78c723b420c398b376a209655d9540463ed022a40ff31a0f75b1866b03047944752e14a2d5c50bbe81e9b 23 | Size: 5332906 24 | Filename: pool/stable/2.0.0/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb 25 | -------------------------------------------------------------------------------- /src/apt/snapshots/packhub__apt__index__tests__multiple_architectures.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apt/index.rs 3 | expression: release 4 | --- 5 | Origin: . stable 6 | Label: . stable 7 | Suite: stable 8 | Codename: stable 9 | Date: Thu, 1 Jan 1970 00:00:00 +0000 10 | Architectures: amd64 11 | Components: main 12 | Description: Generated by packhub 13 | MD5Sum: 14 | dc34cc78e7ea211c16648cf4d5058b7f 825 main/binary-amd64/Packages 15 | 1a9cbea2e2af0769beb96c646d6800b9 550 main/binary-amd64/Packages.gz 16 | 00cc0450632472e7d052e1adb546c1bc 827 main/binary-arm64/Packages 17 | 11c7ee672e6e5b44a4cf020806404915 553 main/binary-arm64/Packages.gz 18 | 75de33a4ab436ced20b0d1e5549469f1 1654 main/binary-armhf/Packages 19 | 42c55998d67940fc56a5ee3a98af0ba2 757 main/binary-armhf/Packages.gz 20 | 9b9b742e803f55894c2a6620d1186a07 829 main/binary-riscv64/Packages 21 | 31a3d0b27d0423acf874bf54d7b3d0bd 553 main/binary-riscv64/Packages.gz 22 | 2d2d98bc14b6861107bb75aa9951d534 772 main/binary-s390x/Packages 23 | 6c48949d483864dd964dd9638d5e22f4 525 main/binary-s390x/Packages.gz 24 | SHA1: 25 | 89dfdd9fdaa1bfd8f751cae2fbe173ddeb3bc155 825 main/binary-amd64/Packages 26 | 9a5a5187251f3e13875856c26c94f1caf0dc9938 550 main/binary-amd64/Packages.gz 27 | 391b818f94fb1b814df168b1e1de7616f9e931bd 827 main/binary-arm64/Packages 28 | b1af467abebba695db6b38a548edd6f9ee21cdbb 553 main/binary-arm64/Packages.gz 29 | d0ebaf02685f8c37c27ec5e3500a7ab27e334e04 1654 main/binary-armhf/Packages 30 | 7ad164d41bd44745322a577b487785227aef2316 757 main/binary-armhf/Packages.gz 31 | daf93234659323df286b6782b78260cae5080573 829 main/binary-riscv64/Packages 32 | 61f5ba94a9ea9a5e906e967bb312e05c2eb787dd 553 main/binary-riscv64/Packages.gz 33 | 1ff724ce2104db2f44b28c4b4f418310b9679b0f 772 main/binary-s390x/Packages 34 | deeec27a71e6de3df724b40120d47f6b8c560e5f 525 main/binary-s390x/Packages.gz 35 | SHA256: 36 | a77f55baac76d7c608c148cb44d3ebd2729e91b1bc6ce00760e3ab147b4c15e3 825 main/binary-amd64/Packages 37 | 931540243041e730a57498d6fd6e75bf86a3bc1e24feb0950a6373915a298d8a 550 main/binary-amd64/Packages.gz 38 | 20e70c475e8f9fbf9a95209186d2260467b7eec6ffe10ef769b97cb351d6c8d3 827 main/binary-arm64/Packages 39 | b7dc4f77f370332e3e02004b6b52d2b46f176ce9883114323ffdb2e4fd8ed48b 553 main/binary-arm64/Packages.gz 40 | ecec52b2cb2072a8f9c56ad57b59f226a8b3889f82824cd52e491d508c842c4b 1654 main/binary-armhf/Packages 41 | 511aeacdeb5a80ef4d3c0d8704c71c71b8c1da0cd6ff8e44397ffee3d9417bea 757 main/binary-armhf/Packages.gz 42 | 5d92bad48122c49c2360030f8752bd94d5ed17b7eb5b8eea17804b08bce9fa7c 829 main/binary-riscv64/Packages 43 | 02e0553328327a7d4a2a76fb8aa3281365780358db95670988b46358b05d7605 553 main/binary-riscv64/Packages.gz 44 | 0163c0b615421b497de5741f2fb2edaa6c5e2d208439a9e3fba077da183df6df 772 main/binary-s390x/Packages 45 | 813299364721e0b57bb7550564ac10bdf457f10a98d79c3c85f3aad8c94009a4 525 main/binary-s390x/Packages.gz 46 | SHA512: 47 | df7dbdf6b502cb4c5d6b0d25456ea82538c1c0f9d5efb5c618e3341da3ca30e3d18a017f9f73bc7bb6efcb22d5fd0f88d710d14420a77d50d66085fc662c3fef 825 main/binary-amd64/Packages 48 | 1381840a14d3a35fa648e3be35a50f03d07d61e462579fc0f3418e48ccfb71fdc97b7497c7e4d4bcaff2b5d946603a191dbb0362f67fe25e1cbcf38654a41a7f 550 main/binary-amd64/Packages.gz 49 | ae2422d17499a57d8d37bc4320f8c6b0862ec26b27c5d87f98fba88d899f6c3a6e2e78c95cf7417641c48410560560740a115882d8b6bf68d7413bc0b33dfc0a 827 main/binary-arm64/Packages 50 | b969bbc6b1d5fcd7e380f402653c0e7ace7b2209d4a89d55dfc2e1ef3f5dad109e61a236eadbd518422986baf0a8a8cc06e9ad1de01bb352344ce9fdd714dadc 553 main/binary-arm64/Packages.gz 51 | 33242dc0c71ecca24ffab2e05d09f5aca172088e7d33ef4005a08334933d7405dbd7ed32d143b4fbe9e8764fb73d64f17a4e890c391963c2043a30e729cf991e 1654 main/binary-armhf/Packages 52 | b43d1ddb37548cc65f090b72b05163c1203da327717bab930cac3f9fa20ad4bf65113cf4839cabbcfad001a74efec67ac41653bd65caa0024405150d5248b22a 757 main/binary-armhf/Packages.gz 53 | 41c97a30246a2eba40a00db51e4f3cd02dff6b4ea43dc90209170d42a382b9db02fbaf25598e3ff665120e1fc8f62f8692fd1701e0542c6cfe918c4ef5c180d8 829 main/binary-riscv64/Packages 54 | 62e5275713626233f45323ad094a1dadc539ddea3b383ee988ce70691d87249373235162613f6634c3dc20bb4d3fb71facdbaa0b194895a89f2a717a166031f3 553 main/binary-riscv64/Packages.gz 55 | b8e34ef8efea71ae3a7e3861491c11833d7de76eb41e9f1a3b81d2bb884c914ce6ded05909935892845a9378fa08d0262fbe6cf946832371a5be540f7f38ce13 772 main/binary-s390x/Packages 56 | c94de85906e90d95af7f5d2cf593721bf79e12ef84dd2cc3ae8250992b65693680be95d989ff659d6a25c360f5a37b7ef5f984f069947e9ee6f8d373a58a1999 525 main/binary-s390x/Packages.gz 57 | -------------------------------------------------------------------------------- /src/apt/snapshots/packhub__apt__index__tests__multiple_packages-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apt/index.rs 3 | expression: release 4 | --- 5 | Origin: . stable 6 | Label: . stable 7 | Suite: stable 8 | Codename: stable 9 | Date: Thu, 1 Jan 1970 00:00:00 +0000 10 | Architectures: amd64 11 | Components: main 12 | Description: Generated by packhub 13 | MD5Sum: 14 | d7018c22c4ba80f82a65a51838a2de04 2729 main/binary-amd64/Packages 15 | ea29d1a5bb92d3ca67d4255fba2d9b9d 1105 main/binary-amd64/Packages.gz 16 | SHA1: 17 | e174e07e3935d1fd9369d326314c20c15d2cb00f 2729 main/binary-amd64/Packages 18 | 674871aef664938568c16f5c3b47a80a0e3819b4 1105 main/binary-amd64/Packages.gz 19 | SHA256: 20 | 88ed2749512fe49f8c06c98e594885244b10bcec68baf5ba3957648dac7f167e 2729 main/binary-amd64/Packages 21 | 5e1e907053cd26e11fa95cb4b9eb8cbcd1bd4038dad36486105a07d8698cb0f7 1105 main/binary-amd64/Packages.gz 22 | SHA512: 23 | 7c35acfb5aa3643ccf052a1f3dd631d1a660866558579cece5f2b189dc94afe52645ad927711cb4bd89be8ce47df2e03b3f42ec5563368993d951fa576d823d3 2729 main/binary-amd64/Packages 24 | 458cd606edd7bbcd34ce5c8e87285d7e2ec0d572dfedc2d40c78d5601cc60df513bd5e22d55e2299b15cb0f809a44751cdfe708dbcafa462942871b0dad5d325 1105 main/binary-amd64/Packages.gz 25 | -------------------------------------------------------------------------------- /src/apt/snapshots/packhub__apt__index__tests__multiple_packages.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apt/index.rs 3 | expression: packages 4 | --- 5 | Architecture: amd64 6 | Depends: fcitx5 (>= 5.0.5), libc6 (>= 2.29), libfcitx5config6 (>= 5.0.5), libfcitx5core7 (>= 5.0.5), libfcitx5utils2 (>= 5.0.5), libgcc-s1 (>= 4.2), libqt5core5a (>= 5.15.1), libqt5gui5 (>= 5.0.2) | libqt5gui5-gles (>= 5.0.2), libqt5network5 (>= 5.0.2), libqt5widgets5 (>= 5.0.2), libstdc++6 (>= 9), libzstd1 (>= 1.4.0) 7 | Description: OpenSource Bengali input method 8 | OpenBangla Keyboard is an OpenSource, Unicode compliant Bengali Input Method for GNU/Linux systems. It's a full fledged Bengali input method with typing automation tools, includes many famous typing methods such as Avro Phonetic, Probhat, Munir Optima, National (Jatiya) etc. 9 | . 10 | Most of the features of Avro Keyboard are present in OpenBangla Keyboard. So Avro Keyboard users will feel at home with OpenBangla Keyboard in Linux. 11 | Homepage: https://openbangla.github.io/ 12 | Maintainer: OpenBangla Team 13 | Package: fcitx-openbangla 14 | Priority: optional 15 | Section: utils 16 | Version: 3.0.0 17 | Installed-Size: 12763 18 | MD5sum: 45ae5eb935c9b9b5fc4df927217a55e3 19 | SHA1: bff2bedc76feb324ea2db323eaa568f2234b92a5 20 | SHA256: c84f27813eebf33fc2b90d3fc330cea5167d0fa08d0a674ac863d207916a33dd 21 | SHA512: 555d2a214ad5d5bd196df952b84c071ecf46502e04af5a715c0b0c65677becc87b08de91e8385d1ab39a80d2aa51dd6ce66a64e0b806a8c16c4a52e67c431b06 22 | Size: 5484440 23 | Filename: pool/stable/3.0.0/fcitx-openbangla_3.0.0.deb 24 | 25 | Architecture: amd64 26 | Depends: ibus (>= 1.5.1), libc6 (>= 2.29), libgcc-s1 (>= 4.2), libglib2.0-0 (>= 2.12.0), libibus-1.0-5 (>= 1.5.1), libqt5core5a (>= 5.12.2), libqt5gui5 (>= 5.0.2) | libqt5gui5-gles (>= 5.0.2), libqt5network5 (>= 5.0.2), libqt5widgets5 (>= 5.0.2), libstdc++6 (>= 5.2), libzstd1 (>= 1.3.2) 27 | Description: OpenSource Bengali input method 28 | OpenBangla Keyboard is an OpenSource, Unicode compliant Bengali Input Method for GNU/Linux systems. It's a full fledged Bengali input method with typing automation tools, includes many famous typing methods such as Avro Phonetic, Probhat, Munir Optima, National (Jatiya) etc. 29 | . 30 | Most of the features of Avro Keyboard are present in OpenBangla Keyboard. So Avro Keyboard users will feel at home with OpenBangla Keyboard in Linux. 31 | . 32 | Homepage: https://openbangla.github.io/ 33 | Maintainer: OpenBangla Team 34 | Package: ibus-openbangla 35 | Priority: optional 36 | Section: utils 37 | Version: 3.0.0 38 | Installed-Size: 12824 39 | MD5sum: 50b535cfbedaa8a64272db3e85082ec6 40 | SHA1: 769405dd1e9da69217f97cbe14d1632201f74c5b 41 | SHA256: 2649db66a21a9ce3d915d856781b83b37dcd069e8834b047ddb2d559e7e54277 42 | SHA512: 9360281f13177c7c4e0cb4cff52cbf0a0d3491febebe8b8cbd6e95ce4dc24f25ca6a0398a41905638d1ab0739667b1737412ba497c1c4cd25d60015b0f481c1f 43 | Size: 5525884 44 | Filename: pool/stable/3.0.0/ibus-openbangla_3.0.0.deb 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use bson::doc; 2 | use chrono::{DateTime, Utc}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::package::{Data, Package}; 6 | 7 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 8 | pub struct PackageMetadata { 9 | name: String, 10 | #[serde(with = "mongodb::bson::serde_helpers::chrono_datetime_as_bson_datetime")] 11 | created_at: DateTime, 12 | metadata: String, 13 | } 14 | 15 | impl PackageMetadata { 16 | /// Create a new `PackageMetadata` from metadata of a `Package`. 17 | /// 18 | /// `None` is returned if the package metadata is not available. 19 | pub fn from_package(package: &Package) -> Option { 20 | let Data::Metadata(metadata) = package.data() else { 21 | return None; 22 | }; 23 | 24 | Some(Self { 25 | name: package.file_name().to_owned(), 26 | created_at: *package.creation_date(), 27 | metadata, 28 | }) 29 | } 30 | 31 | pub async fn retrieve_from( 32 | collection: &mongodb::Collection, 33 | package: &Package, 34 | ) -> Option { 35 | collection 36 | .find_one(doc! { "name": package.file_name(), "created_at": package.creation_date() }) 37 | .await 38 | .unwrap() 39 | } 40 | 41 | pub fn data(self) -> String { 42 | self.metadata 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use std::fs::read; 49 | 50 | use super::*; 51 | use mongodb::Client; 52 | use testcontainers_modules::{ 53 | mongo::Mongo, 54 | testcontainers::{ContainerAsync, runners::AsyncRunner}, 55 | }; 56 | 57 | use crate::apt::DebianPackage; 58 | 59 | pub async fn setup_mongodb(container: &ContainerAsync) -> Client { 60 | let host = container.get_host().await.unwrap(); 61 | let port = container.get_host_port_ipv4(27017).await.unwrap(); 62 | 63 | mongodb::Client::with_uri_str(&format!("mongodb://{}:{}", host, port)) 64 | .await 65 | .unwrap() 66 | } 67 | 68 | #[tokio::test] 69 | async fn test_retrieval() { 70 | let container = Mongo::default().start().await.unwrap(); 71 | let client = setup_mongodb(&container).await; 72 | 73 | let package = Package::detect_package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb", "2.0.0".to_owned(), "https://github.com/OpenBangla/OpenBangla-Keyboard/releases/download/2.0.0/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb".to_owned(), DateTime::parse_from_rfc3339("2024-07-01T00:00:00Z").unwrap().into()).unwrap(); 74 | let data = read("data/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb").unwrap(); 75 | package.set_package_data(data); 76 | 77 | let _ = DebianPackage::from_package(&package).unwrap(); 78 | 79 | let db = client.database("github"); 80 | let collection = db.collection::("test"); 81 | 82 | let metadata = PackageMetadata::from_package(&package).unwrap(); 83 | 84 | collection.insert_one(&metadata).await.unwrap(); 85 | 86 | let retrieved = PackageMetadata::retrieve_from(&collection, &package) 87 | .await 88 | .unwrap(); 89 | 90 | assert_eq!(metadata, retrieved); 91 | 92 | let non_existent = Package::detect_package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb", "2.0.0".to_owned(), "https://github.com/OpenBangla/OpenBangla-Keyboard/releases/download/2.0.0/OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb".to_owned(), DateTime::parse_from_rfc3339("2024-07-10T00:00:00Z").unwrap().into()).unwrap(); 93 | 94 | assert_eq!( 95 | PackageMetadata::retrieve_from(&collection, &non_existent).await, 96 | None 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/detect.rs: -------------------------------------------------------------------------------- 1 | /// This module is responsible for inferring the distribution and version from a given filename. 2 | use std::{collections::HashMap, sync::LazyLock}; 3 | 4 | use regex::Regex; 5 | 6 | use crate::utils::{Arch, Dist}; 7 | 8 | // Regex to capture the package name (stops before version numbers) 9 | static NAME_RE: LazyLock = 10 | LazyLock::new(|| Regex::new(r"(?i)^([a-z0-9_.-]+?)(?:[-_](?:v?\d.*))").unwrap()); 11 | 12 | // Regex to capture architecture 13 | static ARCH_RE: LazyLock = LazyLock::new(|| { 14 | Regex::new(r"(?i)(x86_64|amd64|aarch64|arm64|armhf|armv7|s390x|armv6l|ppc64le|riscv64)") 15 | .unwrap() 16 | }); 17 | 18 | static DISTRO_PATTERNS: LazyLock> = LazyLock::new(|| { 19 | vec![ 20 | // Fedora (fc followed by digits) 21 | (Regex::new(r"fc(\d+)").unwrap(), Dist::Fedora(None)), 22 | // Fedora (fedora followed by optional hyphen and digits) 23 | (Regex::new(r"fedora-?(\d+)?").unwrap(), Dist::Fedora(None)), 24 | // openSUSE Leap (lp followed by digits and decimal) 25 | (Regex::new(r"lp(\d+\.\d+)").unwrap(), Dist::Leap(None)), 26 | // openSUSE Leap (opensuse-leap followed by optional hyphen and version) 27 | ( 28 | Regex::new(r"opensuse-leap-?(\d+\.\d+)").unwrap(), 29 | Dist::Leap(None), 30 | ), 31 | // Debian (debian followed by optional hyphen and digits) 32 | (Regex::new(r"debian-?(\d+)").unwrap(), Dist::Debian(None)), 33 | // Debian (without version) 34 | (Regex::new(r"debian").unwrap(), Dist::Debian(None)), 35 | // Ubuntu (ubuntu followed by optional hyphen and digits with decimal) 36 | ( 37 | Regex::new(r"ubuntu-?(\d+\.\d+)").unwrap(), 38 | Dist::Ubuntu(None), 39 | ), 40 | // Ubuntu (ubuntu followed by optional hyphen and codename) 41 | (Regex::new(r"ubuntu-?([a-z]+)").unwrap(), Dist::Ubuntu(None)), 42 | // Ubuntu (without version) 43 | (Regex::new(r"ubuntu").unwrap(), Dist::Ubuntu(None)), 44 | (Regex::new(r"(?i)suse").unwrap(), Dist::Tumbleweed), 45 | (Regex::new(r"(tw|tumbleweed)").unwrap(), Dist::Tumbleweed), 46 | ] 47 | }); 48 | 49 | static UBUNTU_CODENAMES: LazyLock> = LazyLock::new(|| { 50 | [ 51 | ("precise", "12.04"), 52 | ("trusty", "14.04"), 53 | ("xenial", "16.04"), 54 | ("bionic", "18.04"), 55 | ("focal", "20.04"), 56 | ("jammy", "22.04"), 57 | ("lunar", "23.04"), 58 | ("mantic", "23.10"), 59 | ("noble", "24.04"), 60 | ] 61 | .into() 62 | }); 63 | 64 | /// Package information gained from the filename 65 | #[derive(Debug, PartialEq)] 66 | pub struct PackageInfo { 67 | pub name: Option, 68 | pub distro: Option, 69 | pub architecture: Option, 70 | } 71 | 72 | impl PackageInfo { 73 | pub fn parse_package(filename: &str) -> PackageInfo { 74 | let mut name = None; 75 | let mut architecture = None; 76 | let mut distro = None; 77 | 78 | // Extract package name (e.g., "notes" from "notes-2.3.1...") 79 | if let Some(caps) = NAME_RE.captures(filename) { 80 | name = caps.get(1).map(|m| m.as_str().to_string()); 81 | } 82 | 83 | // As we have the name, we can remove it from the filename. 84 | // To reduce the chance of other regexes matching the name. 85 | let filename = filename.trim_start_matches(name.as_deref().unwrap_or_default()); 86 | 87 | // Extract architecture (e.g., "x86_64") 88 | if let Some(caps) = ARCH_RE.captures(filename) { 89 | architecture = caps 90 | .get(1) 91 | .and_then(|m| m.as_str().to_lowercase().parse().ok()); 92 | } 93 | 94 | // Extract distro and version (e.g., "fedora" and "38") 95 | for (re, dist) in DISTRO_PATTERNS.iter() { 96 | if let Some(caps) = re.captures(filename) { 97 | let mut dist = dist.clone(); 98 | let version = caps.get(1).map(|m| m.as_str()); 99 | 100 | // Check if the distro is Ubuntu and map the codename to version 101 | if let Dist::Ubuntu(_) = dist { 102 | if let Some(codename) = version { 103 | if let Some(ver) = UBUNTU_CODENAMES.get(codename) { 104 | dist.set_version(Some(ver)); 105 | } else { 106 | dist.set_version(Some(codename)); 107 | } 108 | } 109 | } else { 110 | dist.set_version(version); 111 | } 112 | 113 | distro = Some(dist); 114 | break; // Stop at first match 115 | } 116 | } 117 | 118 | PackageInfo { 119 | name, 120 | distro, 121 | architecture, 122 | } 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use super::*; 129 | 130 | #[test] 131 | fn test_name_extraction() { 132 | let cases = vec![ 133 | ("notes-2.3.1-1.x86_64.rpm", "notes"), 134 | ("OpenBangla-Keyboard_2.0.0.deb", "OpenBangla-Keyboard"), 135 | ("caprine_2.60.3_amd64.deb", "caprine"), 136 | ("rustdesk-1.3.8-aarch64.deb", "rustdesk"), 137 | ("myapp-v1.2.3.deb", "myapp"), 138 | ("special_pkg-v2.3_arm64.deb", "special_pkg"), 139 | ]; 140 | 141 | for (filename, expected) in cases { 142 | let info = PackageInfo::parse_package(filename); 143 | assert_eq!( 144 | info.name, 145 | Some(expected.to_string()), 146 | "Failed for: {}", 147 | filename 148 | ); 149 | } 150 | } 151 | 152 | #[test] 153 | fn test_fedora_38() { 154 | let info = PackageInfo::parse_package("notes-2.3.1-1.x86_64-qt6-fedora-38.rpm"); 155 | assert_eq!(info.name, Some("notes".into())); 156 | assert_eq!(info.distro, Some(Dist::fedora("38"))); 157 | assert_eq!(info.architecture, Some(Arch::Amd64)); 158 | } 159 | 160 | #[test] 161 | fn test_fedora38_short() { 162 | let info = PackageInfo::parse_package("OpenBangla-Keyboard_2.0.0-fedora38.rpm"); 163 | assert_eq!(info.name, Some("OpenBangla-Keyboard".into())); 164 | assert_eq!(info.distro, Some(Dist::fedora("38"))); 165 | assert_eq!(info.architecture, None); 166 | } 167 | 168 | #[test] 169 | fn test_opensuse_leap() { 170 | let info = PackageInfo::parse_package("flameshot-12.1.0-1-lp15.2.x86_64.rpm"); 171 | assert_eq!(info.name, Some("flameshot".into())); 172 | assert_eq!(info.distro, Some(Dist::leap("15.2"))); 173 | assert_eq!(info.architecture, Some(Arch::Amd64)); 174 | } 175 | 176 | #[test] 177 | fn test_ubuntu_jammy() { 178 | let info = PackageInfo::parse_package("notes_2.3.1_amd64-qt6-ubuntu-jammy.deb"); 179 | assert_eq!(info.name, Some("notes".into())); 180 | assert_eq!(info.distro, Some(Dist::ubuntu("22.04"))); 181 | assert_eq!(info.architecture, Some(Arch::Amd64)); 182 | } 183 | 184 | #[test] 185 | fn test_debian10() { 186 | let info = PackageInfo::parse_package("flameshot-12.1.0-1.debian-10.amd64.deb"); 187 | assert_eq!(info.name, Some("flameshot".into())); 188 | assert_eq!(info.distro, Some(Dist::debian("10"))); 189 | assert_eq!(info.architecture, Some(Arch::Amd64)); 190 | } 191 | 192 | #[test] 193 | fn test_suse() { 194 | let info = PackageInfo::parse_package("rustdesk-1.3.8-0.aarch64-suse.rpm"); 195 | assert_eq!(info.name, Some("rustdesk".into())); 196 | assert_eq!(info.distro, Some(Dist::Tumbleweed)); 197 | assert_eq!(info.architecture, Some(Arch::Aarch64)); 198 | } 199 | 200 | #[test] 201 | fn test_opensuse_tumbleweed_full_pattern() { 202 | let filename = "another-package-2.3.4-1.tumbleweed.noarch.rpm"; 203 | let result = PackageInfo::parse_package(filename); 204 | assert_eq!(result.name, Some("another-package".into())); 205 | assert_eq!(result.distro, Some(Dist::Tumbleweed)); 206 | } 207 | 208 | #[test] 209 | fn test_distro_without_version() { 210 | let info = PackageInfo::parse_package("ibus-openbangla_3.0.0-fedora.rpm"); 211 | assert_eq!(info.name, Some("ibus-openbangla".into())); 212 | assert_eq!(info.distro, Some(Dist::Fedora(None))); 213 | assert_eq!(info.architecture, None); 214 | 215 | let info = PackageInfo::parse_package("ibus-openbangla_3.0.0-ubuntu.deb"); 216 | assert_eq!(info.distro, Some(Dist::Ubuntu(None))); 217 | 218 | let info = PackageInfo::parse_package("ibus-openbangla_3.0.0-debian.deb"); 219 | assert_eq!(info.distro, Some(Dist::Debian(None))); 220 | } 221 | 222 | #[test] 223 | fn test_caprine() { 224 | let info = PackageInfo::parse_package("caprine_2.60.3_amd64.deb"); 225 | assert_eq!(info.name, Some("caprine".into())); 226 | assert_eq!(info.distro, None); // No distro pattern matched 227 | assert_eq!(info.architecture, Some(Arch::Amd64)); 228 | } 229 | 230 | #[test] 231 | fn test_detect_arch() { 232 | let info = PackageInfo::parse_package("fastfetch-linux-s390x.deb"); 233 | assert_eq!(info.architecture, Some(Arch::S390x)); 234 | 235 | let info = PackageInfo::parse_package("fastfetch-linux-armv6l.deb"); 236 | assert_eq!(info.architecture, Some(Arch::Armhf)); 237 | 238 | let info = PackageInfo::parse_package("fastfetch-linux-aarch64.deb"); 239 | assert_eq!(info.architecture, Some(Arch::Aarch64)); 240 | 241 | let info = PackageInfo::parse_package("fastfetch-linux-armv7l.deb"); 242 | assert_eq!(info.architecture, Some(Arch::Armv7)); 243 | 244 | let info = PackageInfo::parse_package("fastfetch-linux-ppc64le.deb"); 245 | assert_eq!(info.architecture, Some(Arch::PPC64le)); 246 | 247 | let info = PackageInfo::parse_package("fastfetch-linux-amd64.deb"); 248 | assert_eq!(info.architecture, Some(Arch::Amd64)); 249 | 250 | let info = PackageInfo::parse_package("fastfetch-linux-riscv64.deb"); 251 | assert_eq!(info.architecture, Some(Arch::RiscV64)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::response::{IntoResponse, Response}; 3 | use tracing::error; 4 | 5 | pub struct AppError(anyhow::Error); 6 | 7 | impl IntoResponse for AppError { 8 | fn into_response(self) -> Response { 9 | error!("Something went wrong: {}", self.0); 10 | ( 11 | StatusCode::NOT_FOUND, 12 | format!("Something went wrong: {}", self.0), 13 | ) 14 | .into_response() 15 | } 16 | } 17 | 18 | impl From for AppError 19 | where 20 | E: Into, 21 | { 22 | fn from(err: E) -> Self { 23 | Self(err.into()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::LazyLock, time::Duration}; 2 | 3 | use axum::{ 4 | Router, 5 | body::{Body, HttpBody}, 6 | http::Response, 7 | }; 8 | use tower_http::{ 9 | services::{ServeDir, ServeFile}, 10 | trace::TraceLayer, 11 | }; 12 | use tracing::{Span, debug}; 13 | 14 | use crate::state::AppState; 15 | 16 | mod apt; 17 | mod db; 18 | mod detect; 19 | mod error; 20 | mod package; 21 | pub mod pgp; 22 | mod platform; 23 | mod repository; 24 | mod rpm; 25 | mod script; 26 | mod selector; 27 | pub mod state; 28 | mod utils; 29 | 30 | static REQWEST: LazyLock = LazyLock::new(|| { 31 | reqwest::ClientBuilder::new() 32 | .use_rustls_tls() 33 | .build() 34 | .unwrap() 35 | }); 36 | 37 | fn v1() -> Router { 38 | Router::new() 39 | .nest("/apt", apt::apt_routes()) 40 | .nest("/rpm", rpm::rpm_routes()) 41 | .nest("/keys", pgp::keys()) 42 | } 43 | 44 | pub fn app(state: AppState) -> Router { 45 | Router::new() 46 | .route_service("/", ServeFile::new("pages/index.html")) 47 | .nest("/v1", v1()) 48 | .nest("/sh", script::script_routes()) 49 | .nest_service("/assets", ServeDir::new("pages/assets")) 50 | .with_state(state) 51 | .layer(TraceLayer::new_for_http().on_response( 52 | |response: &Response, latency: Duration, _: &Span| { 53 | let size = response.body().size_hint().upper().unwrap_or(0); 54 | let status = response.status(); 55 | debug!(size=size,latency=?latency,status=%status, "finished processing request"); 56 | }, 57 | )) 58 | } 59 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env::args, net::SocketAddr}; 2 | 3 | use axum_server::tls_rustls::RustlsConfig; 4 | use dotenvy::{dotenv, var}; 5 | use tracing::{Level, info}; 6 | use tracing_subscriber::{filter::Targets, prelude::*}; 7 | 8 | use packhub::{app, state::AppState}; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let filter = Targets::new() 13 | .with_target("tower_http", Level::TRACE) 14 | .with_target("packhub", Level::TRACE) 15 | .with_default(Level::INFO); 16 | 17 | tracing_subscriber::registry() 18 | .with(tracing_subscriber::fmt::layer()) 19 | .with(filter) 20 | .init(); 21 | 22 | rustls::crypto::aws_lc_rs::default_provider() 23 | .install_default() 24 | .unwrap(); 25 | 26 | if dotenv().is_err() { 27 | info!("No .env file found"); 28 | } 29 | 30 | let mut generate_keys = false; 31 | 32 | if args().len() > 1 { 33 | let arg = args().nth(1).unwrap(); 34 | if arg == "--generate-keys" { 35 | generate_keys = true; 36 | } 37 | } 38 | 39 | let state = AppState::initialize(generate_keys).await; 40 | 41 | let http_addr: SocketAddr = format!("0.0.0.0:{}", var("PACKHUB_HTTP_PORT").unwrap()) 42 | .parse() 43 | .unwrap(); 44 | 45 | let https_addr: SocketAddr = format!("0.0.0.0:{}", var("PACKHUB_HTTPS_PORT").unwrap()) 46 | .parse() 47 | .unwrap(); 48 | 49 | info!("listening on {}", http_addr); 50 | info!("listening on {}", https_addr); 51 | 52 | let config = RustlsConfig::from_pem_file( 53 | var("PACKHUB_CERT_PEM").unwrap(), 54 | var("PACKHUB_KEY_PEM").unwrap(), 55 | ) 56 | .await 57 | .unwrap(); 58 | 59 | let http_server = axum_server::bind(http_addr).serve(app(state.clone()).into_make_service()); 60 | 61 | let https_server = 62 | axum_server::bind_rustls(https_addr, config).serve(app(state).into_make_service()); 63 | 64 | let http = tokio::spawn(async { http_server.await.unwrap() }); 65 | let https = tokio::spawn(async { https_server.await.unwrap() }); 66 | 67 | _ = tokio::join!(http, https); 68 | } 69 | -------------------------------------------------------------------------------- /src/package.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use anyhow::{Result, bail}; 4 | use chrono::{DateTime, Utc}; 5 | 6 | use crate::{ 7 | REQWEST, 8 | detect::PackageInfo, 9 | utils::{Arch, Dist, Type}, 10 | }; 11 | 12 | struct InnerPackage { 13 | tipe: Type, 14 | info: PackageInfo, 15 | url: String, 16 | ver: String, 17 | data: Mutex, 18 | created: DateTime, 19 | } 20 | 21 | #[derive(Clone, PartialEq)] 22 | pub struct Package { 23 | inner: Arc, 24 | } 25 | 26 | /// Data type for package data and metadata. 27 | /// 28 | /// It is used to differentiate between package data and metadata. 29 | /// 30 | /// `Data::Package` is used for package data. It is the actual package 31 | /// file which needs to be processed to extract the metadata. 32 | /// 33 | /// `Data::Metadata` is used for package metadata. 34 | /// 35 | /// `Data::None` is used when no data is available. 36 | #[derive(Clone, PartialEq)] 37 | pub enum Data { 38 | Package(Vec), 39 | Metadata(String), 40 | None, 41 | } 42 | 43 | impl std::fmt::Debug for Package { 44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 45 | write!(f, "{}", self.file_name()) 46 | } 47 | } 48 | 49 | impl Eq for Package {} 50 | 51 | impl PartialOrd for Package { 52 | fn partial_cmp(&self, other: &Self) -> Option { 53 | Some(self.cmp(other)) 54 | } 55 | } 56 | 57 | impl Ord for Package { 58 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 59 | self.file_name().cmp(other.file_name()) 60 | } 61 | } 62 | 63 | impl PartialEq for InnerPackage { 64 | fn eq(&self, other: &Self) -> bool { 65 | self.tipe == other.tipe 66 | && self.info == other.info 67 | && self.url == other.url 68 | && self.ver == other.ver 69 | && *self.data.lock().unwrap() == *other.data.lock().unwrap() 70 | && self.created == other.created 71 | } 72 | } 73 | 74 | impl Package { 75 | pub fn detect_package( 76 | name: &str, 77 | ver: String, 78 | url: String, 79 | created: DateTime, 80 | ) -> Result { 81 | // Split the extension first. 82 | // If we don't recognize it, then return error. 83 | let Some(tipe) = split_extention(name) else { 84 | bail!("Unknown package type: {}", name); 85 | }; 86 | 87 | let info = PackageInfo::parse_package(name); 88 | 89 | let inner = InnerPackage { 90 | tipe, 91 | info, 92 | url, 93 | ver, 94 | data: Mutex::new(Data::None), 95 | created, 96 | }; 97 | 98 | Ok(Self { 99 | inner: Arc::new(inner), 100 | }) 101 | } 102 | 103 | /// Name of the package 104 | pub fn name(&self) -> Option<&str> { 105 | self.inner.info.name.as_deref() 106 | } 107 | 108 | /// Architecture of the package. 109 | /// By default, it is `amd64`. 110 | pub fn architecture(&self) -> Arch { 111 | self.inner 112 | .info 113 | .architecture 114 | .as_ref() 115 | .cloned() 116 | .unwrap_or_default() 117 | } 118 | 119 | pub fn ty(&self) -> &Type { 120 | &self.inner.tipe 121 | } 122 | 123 | /// Return the distribution for which it was packaged 124 | pub fn distribution(&self) -> Option<&Dist> { 125 | self.inner.info.distro.as_ref() 126 | } 127 | 128 | /// Version of the package 129 | pub fn version(&self) -> &str { 130 | &self.inner.ver 131 | } 132 | 133 | pub fn download_url(&self) -> &str { 134 | &self.inner.url 135 | } 136 | 137 | pub fn file_name(&self) -> &str { 138 | self.inner.url.split('/').last().unwrap() 139 | } 140 | 141 | /// Download package data 142 | /// 143 | /// It is required to call this function before calling the `data()` function. 144 | pub async fn download(&self) -> Result<()> { 145 | let data = REQWEST 146 | .get(self.download_url()) 147 | .send() 148 | .await? 149 | .bytes() 150 | .await?; 151 | *self.inner.data.lock().unwrap() = Data::Package(data.to_vec()); 152 | Ok(()) 153 | } 154 | 155 | /// Return the data of the package. 156 | /// 157 | /// It is required to call the `download()` or `set_metadata()` function before calling this. 158 | /// Otherwise, `None` is returned. 159 | pub fn data(&self) -> Data { 160 | self.inner.data.lock().unwrap().clone() 161 | } 162 | 163 | #[cfg(test)] 164 | /// Set the package data. 165 | /// 166 | /// It's for testing purpose. 167 | pub fn set_package_data(&self, data: Vec) { 168 | *self.inner.data.lock().unwrap() = Data::Package(data); 169 | } 170 | 171 | pub fn creation_date(&self) -> &DateTime { 172 | &self.inner.created 173 | } 174 | 175 | /// Set the package metadata. 176 | pub fn set_metadata(&self, metadata: String) { 177 | *self.inner.data.lock().unwrap() = Data::Metadata(metadata); 178 | } 179 | 180 | /// Check if metadata is available. 181 | pub fn is_metadata_available(&self) -> bool { 182 | matches!(*self.inner.data.lock().unwrap(), Data::Metadata(_)) 183 | } 184 | } 185 | 186 | fn split_extention(s: &str) -> Option { 187 | let mut str = String::with_capacity(3); 188 | let mut index = 0; 189 | 190 | for (idx, ch) in s.char_indices().rev() { 191 | if ch == '.' { 192 | index = idx; 193 | break; 194 | } else { 195 | str.push(ch); 196 | } 197 | } 198 | 199 | if index == 0 { 200 | return None; 201 | } 202 | 203 | // `str` is in reverse order, so we try to match it reversely. 204 | let tipe = match str.as_str() { 205 | "bed" => Type::Deb, 206 | "mpr" => Type::Rpm, 207 | _ => return None, 208 | }; 209 | 210 | Some(tipe) 211 | } 212 | 213 | #[cfg(test)] 214 | pub(crate) mod tests { 215 | use super::*; 216 | 217 | /// A shorthand for `Package::detect_package()` 218 | /// 219 | /// For testing purpose. 220 | pub(crate) fn package(p: &str) -> Package { 221 | Package::detect_package(p, String::new(), p.to_owned(), chrono::DateTime::UNIX_EPOCH) 222 | .unwrap() 223 | } 224 | 225 | /// A shorthand for `Package::detect_package()` 226 | /// 227 | /// For testing purpose. 228 | pub(crate) fn package_with_ver(p: &str, v: &str) -> Package { 229 | Package::detect_package(p, v.to_owned(), p.to_owned(), chrono::DateTime::UNIX_EPOCH) 230 | .unwrap() 231 | } 232 | 233 | #[test] 234 | fn test_package() { 235 | let pack = Package::detect_package( 236 | "OpenBangla-Keyboard_2.0.0-ubuntu22.04.deb", 237 | "2.0.0".to_owned(), 238 | String::new(), 239 | DateTime::UNIX_EPOCH, 240 | ) 241 | .unwrap(); 242 | assert_eq!(pack.version(), "2.0.0"); 243 | assert_eq!(pack.distribution(), Some(&Dist::ubuntu("22.04"))); 244 | assert_eq!(*pack.ty(), Type::Deb); 245 | 246 | let pack = Package::detect_package( 247 | "OpenBangla-Keyboard_2.0.0-fedora36.rpm", 248 | "2.0.0".to_owned(), 249 | String::new(), 250 | DateTime::UNIX_EPOCH, 251 | ) 252 | .unwrap(); 253 | assert_eq!(pack.version(), "2.0.0"); 254 | assert_eq!(pack.distribution(), Some(&Dist::fedora("36"))); 255 | assert_eq!(*pack.ty(), Type::Rpm); 256 | 257 | let pack = Package::detect_package( 258 | "caprine_2.56.1_amd64.deb", 259 | "v2.56.1".to_owned(), 260 | String::new(), 261 | DateTime::UNIX_EPOCH, 262 | ) 263 | .unwrap(); 264 | assert_eq!(pack.version(), "v2.56.1"); 265 | assert_eq!(pack.distribution(), None); 266 | assert_eq!(*pack.ty(), Type::Deb); 267 | 268 | let pack = Package::detect_package( 269 | "ibus-openbangla_3.0.0-opensuse-tumbleweed.rpm", 270 | "3.0.0".to_owned(), 271 | String::new(), 272 | DateTime::UNIX_EPOCH, 273 | ) 274 | .unwrap(); 275 | assert_eq!(pack.version(), "3.0.0"); 276 | assert_eq!(pack.distribution(), Some(&Dist::Tumbleweed)); 277 | assert_eq!(*pack.ty(), Type::Rpm); 278 | } 279 | 280 | #[test] 281 | fn test_package_change_propagation() { 282 | let pack = Package::detect_package( 283 | "OpenBangla-Keyboard_2.0.0-ubuntu22.04.deb", 284 | "2.0.0".to_owned(), 285 | String::new(), 286 | DateTime::UNIX_EPOCH, 287 | ) 288 | .unwrap(); 289 | 290 | assert!(!pack.is_metadata_available()); 291 | 292 | let pack2 = pack.clone(); 293 | pack2.set_metadata(String::new()); 294 | 295 | assert!(pack.is_metadata_available()); 296 | } 297 | 298 | #[test] 299 | fn test_split_extension() { 300 | assert_eq!( 301 | split_extention("OpenBangla-Keyboard_2.0.0-ubuntu22.04.deb"), 302 | Some(Type::Deb) 303 | ); 304 | assert_eq!( 305 | split_extention("OpenBangla-Keyboard_2.0.0-fedora36.rpm"), 306 | Some(Type::Rpm) 307 | ); 308 | assert_eq!(split_extention("caprine_2.56.1_amd64.snap"), None); 309 | assert_eq!(split_extention("deb"), None); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/pgp.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io::Write}; 2 | 3 | use anyhow::Result; 4 | use axum::{Router, extract::State, routing::get}; 5 | use sequoia_openpgp::{ 6 | cert::prelude::*, 7 | crypto::Password, 8 | parse::Parse, 9 | policy::StandardPolicy, 10 | serialize::{ 11 | SerializeInto, 12 | stream::{Armorer, Message, Signer}, 13 | }, 14 | }; 15 | 16 | use crate::state::AppState; 17 | 18 | fn generate_keys(passphrase: &Password) -> Result { 19 | let (cert, _) = CertBuilder::new() 20 | .add_userid("PackHub ") 21 | .set_password(Some(passphrase.clone())) 22 | .add_signing_subkey() 23 | .generate()?; 24 | 25 | Ok(cert) 26 | } 27 | 28 | pub fn generate_and_save_keys(passphrase: &Password) -> Result { 29 | let cert = generate_keys(passphrase)?; 30 | 31 | let key = cert.as_tsk().to_vec()?; 32 | 33 | fs::write("key.gpg", key)?; 34 | 35 | Ok(cert) 36 | } 37 | 38 | pub fn load_cert_from_file() -> Result { 39 | let key = fs::read("key.gpg")?; 40 | let cert = Cert::from_bytes(&key)?; 41 | 42 | Ok(cert) 43 | } 44 | 45 | pub fn clearsign_metadata(data: &str, cert: &Cert, passphrase: &Password) -> Result> { 46 | let binding = StandardPolicy::new(); 47 | let key = cert 48 | .keys() 49 | .secret() 50 | .with_policy(&binding, None) 51 | .supported() 52 | .alive() 53 | .revoked(false) 54 | .for_signing() 55 | .next() 56 | .unwrap() 57 | .key() 58 | .clone(); 59 | 60 | let decrypted_key = key.decrypt_secret(passphrase)?; 61 | let keypair = decrypted_key.into_keypair()?; 62 | 63 | let mut sink = vec![]; 64 | let message = Message::new(&mut sink); 65 | let mut signer = Signer::new(message, keypair)?.cleartext().build()?; 66 | 67 | signer.write_all(data.as_bytes())?; 68 | signer.finalize()?; 69 | 70 | Ok(sink) 71 | } 72 | 73 | pub fn detached_sign_metadata(content: &str, cert: &Cert, passphrase: &Password) -> Result> { 74 | let binding = StandardPolicy::new(); 75 | let key = cert 76 | .keys() 77 | .secret() 78 | .with_policy(&binding, None) 79 | .supported() 80 | .alive() 81 | .revoked(false) 82 | .for_signing() 83 | .next() 84 | .unwrap() 85 | .key() 86 | .clone(); 87 | 88 | let decrypted_key = key.decrypt_secret(passphrase)?; 89 | let keypair = decrypted_key.into_keypair()?; 90 | 91 | let mut sink = vec![]; 92 | let message = Armorer::new(Message::new(&mut sink)).build()?; 93 | let mut signer = Signer::new(message, keypair)?.detached().build()?; 94 | 95 | signer.write_all(content.as_bytes())?; 96 | signer.finalize()?; 97 | 98 | Ok(sink) 99 | } 100 | 101 | /////////////////////////////////////// Axum handlers ///////////////////////////////////////////////// 102 | 103 | async fn armored_public_key_handler(State(state): State) -> Vec { 104 | state.armored_public_key() 105 | } 106 | 107 | async fn dearmored_public_key_handler(State(state): State) -> Vec { 108 | state.dearmored_public_key() 109 | } 110 | 111 | pub fn keys() -> Router { 112 | Router::new() 113 | .route("/packhub.asc", get(armored_public_key_handler)) 114 | .route("/packhub.gpg", get(dearmored_public_key_handler)) 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use std::io::Read; 120 | 121 | use anyhow::Result; 122 | use sequoia_openpgp::{ 123 | cert::prelude::*, 124 | parse::Parse, 125 | parse::stream::{MessageLayer, MessageStructure, VerificationHelper, VerifierBuilder}, 126 | policy::StandardPolicy, 127 | }; 128 | 129 | use super::*; 130 | 131 | struct VerificationHelperImpl { 132 | public_key: Cert, 133 | } 134 | 135 | impl VerificationHelper for VerificationHelperImpl { 136 | fn get_certs( 137 | &mut self, 138 | _ids: &[sequoia_openpgp::KeyHandle], 139 | ) -> sequoia_openpgp::Result> { 140 | Ok(vec![self.public_key.clone()]) 141 | } 142 | 143 | fn check(&mut self, structure: MessageStructure<'_>) -> sequoia_openpgp::Result<()> { 144 | for layer in structure.into_iter() { 145 | match layer { 146 | MessageLayer::SignatureGroup { ref results } => { 147 | // Simply check if all signatures are valid 148 | if !results.iter().any(|r| r.is_ok()) { 149 | return Err(anyhow::anyhow!("No valid signature")); 150 | } 151 | } 152 | _ => {} 153 | } 154 | } 155 | Ok(()) 156 | } 157 | } 158 | 159 | #[test] 160 | fn test_pgp_sign_and_verify() -> Result<()> { 161 | let passphrase = "secure-passphrase".into(); 162 | 163 | // Generate new PGP key pair 164 | let cert = generate_keys(&passphrase)?; 165 | let message = "Test message to be signed"; 166 | 167 | // Sign the message using cleartext signing 168 | let signed_message = clearsign_metadata(message, &cert, &passphrase)?; 169 | 170 | // Set up verification 171 | let helper = VerificationHelperImpl { 172 | public_key: cert.clone(), 173 | }; 174 | let policy = StandardPolicy::new(); 175 | 176 | // Create verifier with our helper 177 | let mut verifier = VerifierBuilder::from_bytes(&signed_message)? 178 | .with_policy(&policy, None, helper)?; 179 | 180 | // Read the verified content 181 | let mut verified_content = Vec::new(); 182 | verifier.read_to_end(&mut verified_content)?; 183 | let verified_text = String::from_utf8(verified_content)?; 184 | 185 | // Verify the content matches original message 186 | assert_eq!(verified_text.trim(), message.trim()); 187 | 188 | Ok(()) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/platform.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::sync::LazyLock; 3 | 4 | use lenient_semver::parse; 5 | use regex::Regex; 6 | use semver::{Version, VersionReq}; 7 | 8 | use crate::{REQWEST, utils::Dist}; 9 | 10 | static PRE_RELEASE_STRIPER: LazyLock = LazyLock::new(|| Regex::new(r"(\d+)\D").unwrap()); 11 | static APT: LazyLock = LazyLock::new(|| Regex::new(r#"Debian APT.+\((.+)\)"#).unwrap()); 12 | static FEDORA: LazyLock = 13 | LazyLock::new(|| Regex::new(r#"libdnf \(Fedora Linux (\d+);"#).unwrap()); 14 | static TUMBLEWEED: LazyLock = 15 | LazyLock::new(|| Regex::new(r#"ZYpp.+openSUSE-Tumbleweed"#).unwrap()); 16 | 17 | /// Detects platform based on the user-agent string of `apt` package manager. 18 | pub struct AptPlatformDetection { 19 | ubuntu: HashMap, 20 | debian: HashMap, 21 | } 22 | 23 | impl AptPlatformDetection { 24 | pub async fn initialize() -> Self { 25 | let data = REQWEST 26 | .get("https://repology.org/api/v1/project/apt") 27 | .send() 28 | .await 29 | .unwrap() 30 | .text() 31 | .await 32 | .unwrap(); 33 | 34 | let data: serde_json::Value = serde_json::from_str(&data).unwrap(); 35 | 36 | // HashSet to remove duplicates 37 | let mut map: HashMap<&str, HashSet> = HashMap::new(); 38 | 39 | for item in data.as_array().unwrap() { 40 | let repo = item["repo"].as_str().unwrap(); 41 | if repo.starts_with("ubuntu") || repo.starts_with("debian") { 42 | let repo = repo.trim_end_matches("_proposed"); 43 | let ver = item["version"].as_str().unwrap(); 44 | let parsed = fresh_version(parse(ver).unwrap()); 45 | map.entry(repo).or_default().insert(parsed); 46 | } 47 | } 48 | 49 | let mut ubuntu = HashMap::new(); 50 | let mut debian = HashMap::new(); 51 | 52 | for (key, value) in map.into_iter() { 53 | let mut versions = value.into_iter().collect::>(); 54 | versions.sort(); 55 | let requirement; 56 | 57 | if versions.len() > 1 { 58 | requirement = VersionReq::parse(&format!( 59 | ">={}, <={}", 60 | versions[0], 61 | versions[versions.len() - 1] 62 | )) 63 | .unwrap(); 64 | } else { 65 | requirement = VersionReq::parse(&format!("={}", versions[0])).unwrap(); 66 | } 67 | 68 | if key.starts_with("ubuntu") { 69 | let ver = key.trim_start_matches("ubuntu_"); 70 | ubuntu.insert(requirement, Dist::ubuntu(&ver.replace("_", "."))); 71 | } else if key.starts_with("debian") { 72 | let ver = key.trim_start_matches("debian_"); 73 | debian.insert(requirement, Dist::debian(ver)); 74 | } 75 | } 76 | 77 | Self { ubuntu, debian } 78 | } 79 | 80 | pub fn detect_ubuntu_for_apt(&self, agent: &str) -> Dist { 81 | let ver = get_apt_version(agent); 82 | let mut dist = Dist::Ubuntu(None); 83 | 84 | let apt = fresh_version(parse(ver).unwrap()); 85 | 86 | for (matcher, dst) in self.ubuntu.iter() { 87 | if matcher.matches(&apt) { 88 | dist = dst.clone(); 89 | break; 90 | } 91 | } 92 | 93 | dist 94 | } 95 | 96 | pub fn detect_debian_for_apt(&self, agent: &str) -> Dist { 97 | let ver = get_apt_version(agent); 98 | let mut dist = Dist::Debian(None); 99 | 100 | let apt = fresh_version(parse(ver).unwrap()); 101 | 102 | for (matcher, dst) in self.debian.iter() { 103 | if matcher.matches(&apt) { 104 | dist = dst.clone(); 105 | break; 106 | } 107 | } 108 | 109 | dist 110 | } 111 | } 112 | 113 | /// Removes the errorneous pre-release or build part from the version. 114 | fn fresh_version(mut ver: Version) -> Version { 115 | ver.build = semver::BuildMetadata::EMPTY; 116 | 117 | let pre = ver.pre.as_str(); 118 | 119 | // `1.0.1ubuntu2.24` is erroneously parsed as `1.0.0-1ubuntu2.24` 120 | // so we need to strip the pre-release part and set the patch correctly 121 | if let Some(capture) = PRE_RELEASE_STRIPER.captures(pre) { 122 | let patch: u64 = capture.get(1).unwrap().as_str().parse().unwrap(); 123 | ver.patch = patch; 124 | ver.pre = semver::Prerelease::EMPTY; 125 | } 126 | 127 | ver 128 | } 129 | 130 | fn get_apt_version(agent: &str) -> &str { 131 | APT.captures(agent).unwrap().get(1).unwrap().as_str() 132 | } 133 | 134 | /// Retrieve the fedora version from the user-agent string. 135 | pub fn get_fedora_version(agent: &str) -> Option<&str> { 136 | Some(FEDORA.captures(agent)?.get(1)?.as_str()) 137 | } 138 | 139 | /// Detect the opensuse fa from the user-agent string. 140 | pub fn detect_opensuse_tumbleweed(agent: &str) -> bool { 141 | TUMBLEWEED.is_match(agent) 142 | } 143 | 144 | pub fn detect_rpm_os(agent: &str) -> Option { 145 | if let Some(ver) = get_fedora_version(agent) { 146 | Some(Dist::fedora(ver)) 147 | } else if detect_opensuse_tumbleweed(agent) { 148 | Some(Dist::Tumbleweed) 149 | } else { 150 | None 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::*; 157 | 158 | #[tokio::test] 159 | async fn test_match_platform() { 160 | let platform = AptPlatformDetection::initialize().await; 161 | 162 | // Ubuntu 163 | assert_eq!( 164 | platform.detect_ubuntu_for_apt("Debian APT-HTTP/1.3 (2.0.2)"), 165 | Dist::ubuntu("20.04") 166 | ); 167 | assert_eq!( 168 | platform.detect_ubuntu_for_apt("Debian APT-HTTP/1.3 (2.0.9)"), 169 | Dist::ubuntu("20.04") 170 | ); 171 | assert_eq!( 172 | platform.detect_ubuntu_for_apt("Debian APT-HTTP/1.3 (2.4.5)"), 173 | Dist::ubuntu("22.04") 174 | ); 175 | assert_eq!( 176 | platform.detect_ubuntu_for_apt("Debian APT-HTTP/1.3 (2.4.8)"), 177 | Dist::ubuntu("22.04") 178 | ); 179 | assert_eq!( 180 | platform.detect_ubuntu_for_apt("Debian APT-HTTP/1.3 (2.4.10)"), 181 | Dist::ubuntu("22.04") 182 | ); 183 | assert_eq!( 184 | platform.detect_ubuntu_for_apt("Debian APT-HTTP/1.3 (2.7.14build2)"), 185 | Dist::ubuntu("24.04") 186 | ); 187 | 188 | // Debian 189 | assert_eq!( 190 | platform.detect_debian_for_apt("Debian APT-HTTP/1.3 (1.8.2.3)"), 191 | Dist::debian("10") 192 | ); 193 | assert_eq!( 194 | platform.detect_debian_for_apt("Debian APT-HTTP/1.3 (2.2.4)"), 195 | Dist::debian("11") 196 | ); 197 | assert_eq!( 198 | platform.detect_debian_for_apt("Debian APT-HTTP/1.3 (2.6.1)"), 199 | Dist::debian("12") 200 | ); 201 | // assert_eq!( 202 | // platform.detect_debian_for_apt("Debian APT-HTTP/1.3 (2.9.23)"), 203 | // Dist::Debian(Some("13".to_owned())) 204 | // ); 205 | } 206 | 207 | #[test] 208 | fn test_apt_version() { 209 | assert_eq!(get_apt_version("Debian APT-HTTP/1.3 (2.5.3)"), "2.5.3"); 210 | } 211 | 212 | #[test] 213 | fn test_fedora_version() { 214 | assert_eq!( 215 | get_fedora_version("libdnf (Fedora Linux 38; container; Linux.x86_64)"), 216 | Some("38") 217 | ); 218 | assert_eq!( 219 | get_fedora_version("libdnf (Fedora Linux 39; container; Linux.x86_64)"), 220 | Some("39") 221 | ); 222 | } 223 | 224 | #[test] 225 | fn test_detect_opensuse() { 226 | assert!(detect_opensuse_tumbleweed( 227 | "ZYpp 17.31.15 (curl 8.5.0) openSUSE-Tumbleweed-x86_64" 228 | )); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/repository.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | use mongodb::Collection; 3 | use tokio::task::JoinSet; 4 | use tracing::{debug, error}; 5 | 6 | use crate::{ 7 | db::PackageMetadata, 8 | package::Package, 9 | platform::{AptPlatformDetection, detect_rpm_os}, 10 | selector::select_packages, 11 | state::AppState, 12 | }; 13 | 14 | pub struct Repository { 15 | collection: Collection, 16 | packages: Vec, 17 | downloaded: Vec, 18 | platform: AptPlatformDetection, 19 | } 20 | 21 | impl Repository { 22 | pub async fn from_github(owner: String, repo: String, state: &AppState) -> Self { 23 | let project = format!("{owner}/{repo}"); 24 | let collection = state 25 | .db() 26 | .database("github") 27 | .collection::(&project); 28 | 29 | let mut packages = Vec::new(); 30 | let release = state 31 | .github() 32 | .repos(owner, repo) 33 | .releases() 34 | .get_latest() 35 | .await 36 | .unwrap(); 37 | 38 | for asset in release.assets { 39 | let package = Package::detect_package( 40 | &asset.name, 41 | release.tag_name.clone(), 42 | asset.browser_download_url.to_string(), 43 | asset.updated_at, 44 | ); 45 | if let Ok(package) = package { 46 | if let Some(metadata) = PackageMetadata::retrieve_from(&collection, &package).await 47 | { 48 | package.set_metadata(metadata.data()); 49 | } 50 | packages.push(package); 51 | } 52 | } 53 | 54 | let platform = AptPlatformDetection::initialize().await; 55 | 56 | Repository { 57 | collection, 58 | packages, 59 | platform, 60 | downloaded: Vec::new(), 61 | } 62 | } 63 | 64 | pub async fn save_package_metadata(&mut self) { 65 | for package in &self.downloaded { 66 | let Some(metadata) = PackageMetadata::from_package(package) else { 67 | error!( 68 | "Metadata was not available for saving the package: {:?}", 69 | package.file_name() 70 | ); 71 | return; 72 | }; 73 | 74 | if let Err(e) = self.collection.insert_one(metadata).await { 75 | error!( 76 | "Failed to save metadata for package: {:?}\n Error: {e}", 77 | package.file_name() 78 | ); 79 | return; 80 | }; 81 | debug!("Saved metadata for package: {:?}", package.file_name()); 82 | } 83 | } 84 | 85 | /// Select packages for apt based distributions. 86 | /// 87 | /// The `distro` parameter is the name of the distribution (`debian`, `ubuntu`). 88 | /// 89 | /// The `agent` parameter is the user-agent string of the apt client. 90 | /// 91 | /// It returns a vector of packages that are compatible with the given agent. 92 | /// 93 | /// It also downloads the selected packages if the metadata is not available. 94 | pub async fn select_package_apt(&mut self, distro: &str, agent: &str) -> Result> { 95 | let dist = match distro { 96 | "ubuntu" => self.platform.detect_ubuntu_for_apt(agent), 97 | "debian" => self.platform.detect_debian_for_apt(agent), 98 | dist => bail!("Unknown apt distribution {dist}"), 99 | }; 100 | 101 | let packages: Vec = select_packages(&self.packages, dist) 102 | .into_iter() 103 | .cloned() 104 | .collect(); 105 | 106 | debug!("Packages selected {:?}", packages); 107 | 108 | self.download_packages(packages).await 109 | } 110 | 111 | /// Select packages for RPM based distribution. 112 | /// 113 | /// The `agent` parameter is the user-agent string of the rpm client. 114 | /// 115 | /// It returns a vector of packages that are compatible with the given agent. 116 | /// 117 | /// It also downloads the selected packages if the metadata is not available. 118 | pub async fn select_package_rpm(&mut self, agent: &str) -> Result> { 119 | let Some(dist) = detect_rpm_os(agent) else { 120 | bail!("Unknown RPM distribution agent: {agent}"); 121 | }; 122 | let packages: Vec = select_packages(&self.packages, dist) 123 | .into_iter() 124 | .cloned() 125 | .collect(); 126 | 127 | debug!("Packages selected {:?}", packages); 128 | 129 | self.download_packages(packages).await 130 | } 131 | 132 | async fn download_packages(&mut self, packages: Vec) -> Result> { 133 | let mut runner = JoinSet::new(); 134 | let mut result = Vec::new(); 135 | 136 | for package in packages { 137 | if !package.is_metadata_available() { 138 | runner.spawn(async move { 139 | debug!("Downloading package: {:?}", package.file_name()); 140 | package.download().await.and_then(|_| Ok(package)) 141 | }); 142 | } else { 143 | debug!("Package metadata available: {:?}", package.file_name()); 144 | result.push(package); 145 | } 146 | } 147 | 148 | while let Some(res) = runner.join_next().await { 149 | let Ok(res) = res else { 150 | bail!("Executor error: Failed to download package") 151 | }; 152 | 153 | let package = res?; 154 | 155 | debug!("Downloaded package: {:?}", package.file_name()); 156 | 157 | result.push(package.clone()); 158 | self.downloaded.push(package); 159 | } 160 | 161 | result.sort(); 162 | 163 | Ok(result) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/rpm/index.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use sha2::Sha256; 3 | use zstd::encode_all; 4 | 5 | use crate::utils::hashsum; 6 | 7 | use super::package::RPMPackage; 8 | 9 | #[derive(Template)] 10 | #[template(path = "primary.xml")] 11 | struct Primary<'a> { 12 | packages: &'a [RPMPackage], 13 | } 14 | 15 | #[derive(Template)] 16 | #[template(path = "filelists.xml")] 17 | struct FileLists<'a> { 18 | packages: &'a [RPMPackage], 19 | } 20 | 21 | #[derive(Template)] 22 | #[template(path = "other.xml")] 23 | struct Other<'a> { 24 | packages: &'a [RPMPackage], 25 | } 26 | 27 | #[derive(Template)] 28 | #[template(path = "repomd.xml")] 29 | struct RepoMD { 30 | primary: Metadata, 31 | filelists: Metadata, 32 | other: Metadata, 33 | timestamp: i64, 34 | } 35 | 36 | struct Metadata { 37 | sha256: String, 38 | open_sha256: String, 39 | size: usize, 40 | open_size: usize, 41 | } 42 | 43 | pub fn get_primary_index(packages: &[RPMPackage]) -> String { 44 | let primary = Primary { packages }; 45 | primary.render().unwrap() 46 | } 47 | 48 | pub fn get_filelists_index(packages: &[RPMPackage]) -> String { 49 | let list = FileLists { packages }; 50 | list.render().unwrap() 51 | } 52 | 53 | pub fn get_other_index(packages: &[RPMPackage]) -> String { 54 | let list = Other { packages }; 55 | list.render().unwrap() 56 | } 57 | 58 | pub fn get_repomd_index(packages: &[RPMPackage]) -> String { 59 | let primary = get_primary_index(packages); 60 | let filelists = get_filelists_index(packages); 61 | let other = get_other_index(packages); 62 | 63 | // Find the latest date from the list of packages 64 | let mut timestamp = 0; 65 | for package in packages { 66 | if package.pkg_time > timestamp { 67 | timestamp = package.pkg_time; 68 | } 69 | } 70 | 71 | let repomd = RepoMD::create(primary, filelists, other, timestamp); 72 | 73 | repomd.render().unwrap() 74 | } 75 | 76 | impl Metadata { 77 | /// Create the metadata of the `content`. 78 | fn create(content: String) -> Metadata { 79 | let data = content.as_bytes(); 80 | let open_size = data.len(); 81 | let open_sha256 = hashsum::(data); 82 | let compressed = encode_all(data, 0).unwrap(); 83 | let size = compressed.len(); 84 | let sha256 = hashsum::(&compressed); 85 | 86 | Metadata { 87 | sha256, 88 | open_sha256, 89 | size, 90 | open_size, 91 | } 92 | } 93 | } 94 | 95 | impl RepoMD { 96 | fn create(primary: String, filelists: String, other: String, timestamp: i64) -> RepoMD { 97 | let primary = Metadata::create(primary); 98 | let filelists = Metadata::create(filelists); 99 | let other = Metadata::create(other); 100 | 101 | RepoMD { 102 | primary, 103 | filelists, 104 | other, 105 | timestamp, 106 | } 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use std::fs::read; 113 | 114 | use chrono::DateTime; 115 | use insta::assert_snapshot; 116 | 117 | use crate::package::{tests::package_with_ver, Package}; 118 | use super::*; 119 | 120 | #[test] 121 | fn test_rpm_indices() { 122 | let package = Package::detect_package("OpenBangla-Keyboard_2.0.0-fedora38.rpm", "2.0.0".to_owned(), "https://github.com/OpenBangla/OpenBangla-Keyboard/releases/download/2.0.0/OpenBangla-Keyboard_2.0.0-fedora38.rpm".to_owned(), DateTime::parse_from_rfc2822("Wed, 8 Nov 2023 16:40:12 +0000").unwrap().into()).unwrap(); 123 | let data = read("data/OpenBangla-Keyboard_2.0.0-fedora38.rpm").unwrap(); 124 | package.set_package_data(data); 125 | let package = RPMPackage::from_package(&package).unwrap(); 126 | let packages = vec![package]; 127 | 128 | assert_snapshot!(get_primary_index(&packages)); 129 | 130 | assert_snapshot!(get_filelists_index(&packages)); 131 | 132 | assert_snapshot!(get_other_index(&packages)); 133 | 134 | assert_snapshot!(get_repomd_index(&packages)); 135 | } 136 | 137 | #[test] 138 | fn test_multiple_arch() { 139 | let package = package_with_ver("fastfetch-linux-aarch64.rpm", "2.40.3"); 140 | let data = read("data/fastfetch-linux-aarch64.rpm").unwrap(); 141 | package.set_package_data(data); 142 | let package1 = RPMPackage::from_package(&package).unwrap(); 143 | 144 | let package = package_with_ver("fastfetch-linux-amd64.rpm", "2.40.3"); 145 | let data = read("data/fastfetch-linux-amd64.rpm").unwrap(); 146 | package.set_package_data(data); 147 | let package2 = RPMPackage::from_package(&package).unwrap(); 148 | 149 | let package = package_with_ver("fastfetch-linux-armv6l.rpm", "2.40.3"); 150 | let data = read("data/fastfetch-linux-armv6l.rpm").unwrap(); 151 | package.set_package_data(data); 152 | let package3 = RPMPackage::from_package(&package).unwrap(); 153 | 154 | let package = package_with_ver("fastfetch-linux-armv7l.rpm", "2.40.3"); 155 | let data = read("data/fastfetch-linux-armv7l.rpm").unwrap(); 156 | package.set_package_data(data); 157 | let package4 = RPMPackage::from_package(&package).unwrap(); 158 | 159 | let package = package_with_ver("fastfetch-linux-ppc64le.rpm", "2.40.3"); 160 | let data = read("data/fastfetch-linux-ppc64le.rpm").unwrap(); 161 | package.set_package_data(data); 162 | let package5 = RPMPackage::from_package(&package).unwrap(); 163 | 164 | let package = package_with_ver("fastfetch-linux-s390x.rpm", "2.40.3"); 165 | let data = read("data/fastfetch-linux-s390x.rpm").unwrap(); 166 | package.set_package_data(data); 167 | let package6 = RPMPackage::from_package(&package).unwrap(); 168 | 169 | let package = package_with_ver("fastfetch-linux-riscv64.rpm", "2.40.3"); 170 | let data = read("data/fastfetch-linux-riscv64.rpm").unwrap(); 171 | package.set_package_data(data); 172 | let package7 = RPMPackage::from_package(&package).unwrap(); 173 | 174 | let packages = vec![ 175 | package1, package2, package3, package4, package5, package6, package7, 176 | ]; 177 | 178 | assert_snapshot!(get_primary_index(&packages)); 179 | assert_snapshot!(get_filelists_index(&packages)); 180 | assert_snapshot!(get_other_index(&packages)); 181 | assert_snapshot!(get_repomd_index(&packages)); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/rpm/mod.rs: -------------------------------------------------------------------------------- 1 | mod index; 2 | mod package; 3 | mod routes; 4 | 5 | pub use self::routes::rpm_routes; 6 | -------------------------------------------------------------------------------- /src/rpm/package.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, bail}; 2 | use rpm::{DependencyFlags, FileMode, IndexSignatureTag}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::{from_str, to_string}; 5 | use sha2::Sha256; 6 | 7 | use crate::{ 8 | package::{Data, Package}, 9 | utils::hashsum, 10 | }; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct RPMPackage { 14 | pub name: String, 15 | pub epoch: u32, 16 | pub version: String, 17 | pub release: String, 18 | pub arch: String, 19 | pub vendor: Option, 20 | pub url: Option, 21 | pub license: Option, 22 | pub summary: Option, 23 | pub description: Option, 24 | pub group: Option, 25 | pub build_time: u64, 26 | pub build_host: Option, 27 | pub source: Option, 28 | pub provides: Vec, 29 | pub requires: Vec, 30 | pub sha256: String, 31 | pub header_start: u64, 32 | pub header_end: u64, 33 | pub files: Vec, 34 | pub pkg_size: usize, 35 | pub installed_size: u64, 36 | pub archive_size: u64, 37 | pub location: String, 38 | pub pkg_time: i64, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct Dependency { 43 | pub dependency: String, 44 | pub version: String, 45 | pub condition: String, 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | pub struct File { 50 | pub path: String, 51 | pub dir: bool, 52 | } 53 | 54 | impl RPMPackage { 55 | /// Parse the package and return the RPM package. 56 | /// 57 | /// Also sets the metadata to the package. 58 | pub fn from_package(package: &Package) -> Result { 59 | // If the metadata is already available, then build the RPMPackage from it 60 | if let Data::Metadata(metadata) = package.data() { 61 | let rpm: RPMPackage = from_str(&metadata).context("Error while loading RPMPackage from saved Package metadata")?; 62 | return Ok(rpm); 63 | } 64 | 65 | let Data::Package(data) = package.data() else { 66 | bail!("Data isn't loaded in package"); 67 | }; 68 | 69 | let mut data = data.as_slice(); 70 | // Calculate these before the data slice is mutated 71 | let pkg_size = data.len(); 72 | let sha256 = hashsum::(data); 73 | 74 | let rpm = rpm::Package::parse(&mut data) 75 | .context("Unable to parse the package using rpm parser crate")?; 76 | let header = rpm.metadata; 77 | 78 | let name = header.get_name()?.to_owned(); 79 | let epoch = header.get_epoch().unwrap_or(0); 80 | let version = header.get_version()?.to_owned(); 81 | let release = header.get_release()?.to_owned(); 82 | let arch = header.get_arch()?.to_owned(); 83 | let vendor = header.get_vendor().ok().map(|v| v.to_owned()); 84 | let url = header.get_url().ok().map(|v| v.to_owned()); 85 | let license = header.get_license().ok().map(|v| v.to_owned()); 86 | let summary = header.get_summary().ok().map(|v| v.to_owned()); 87 | let description = header.get_description().ok().map(|v| v.to_owned()); 88 | let group = header.get_group().ok().map(|v| v.to_owned()); 89 | let build_time = header.get_build_time()?; 90 | let build_host = header.get_build_host().ok().map(|v| v.to_owned()); 91 | let source = header.get_source_rpm().ok().map(|v| v.to_owned()); 92 | let range = header.get_package_segment_offsets(); 93 | let header_start = range.header; 94 | let header_end = range.payload; 95 | let installed_size = header.get_installed_size()?; 96 | let archive_size = header 97 | .signature 98 | .get_entry_data_as_u64(IndexSignatureTag::RPMSIGTAG_LONGARCHIVESIZE) 99 | .or_else(|_e| { 100 | header 101 | .signature 102 | .get_entry_data_as_u32(IndexSignatureTag::RPMSIGTAG_PAYLOADSIZE) 103 | .map(|v| v as u64) 104 | })?; 105 | 106 | let provides: Vec = header 107 | .get_provides()? 108 | .into_iter() 109 | .map(|i| Dependency { 110 | dependency: i.name, 111 | version: i.version, 112 | condition: flag_to_condition(i.flags), 113 | }) 114 | .collect(); 115 | 116 | let requires: Vec = header 117 | .get_requires()? 118 | .into_iter() 119 | .filter(|i| !i.flags.contains(DependencyFlags::RPMLIB)) 120 | .filter(|i| !i.flags.contains(DependencyFlags::CONFIG)) 121 | .map(|i| Dependency { 122 | dependency: i.name, 123 | version: i.version, 124 | condition: flag_to_condition(i.flags), 125 | }) 126 | .collect(); 127 | 128 | let files: Vec = header 129 | .get_file_entries()? 130 | .into_iter() 131 | .map(|i| File { 132 | path: i.path.to_string_lossy().to_string(), 133 | dir: matches!(i.mode, FileMode::Dir { .. }), 134 | }) 135 | .collect(); 136 | 137 | let location = format!("package/{}/{}", package.version(), package.file_name()); 138 | let pkg_time = package.creation_date().timestamp(); 139 | 140 | let rpm = RPMPackage { 141 | name, 142 | epoch, 143 | version, 144 | release, 145 | arch, 146 | vendor, 147 | url, 148 | license, 149 | summary, 150 | description, 151 | group, 152 | build_time, 153 | build_host, 154 | source, 155 | provides, 156 | requires, 157 | sha256, 158 | header_start, 159 | header_end, 160 | files, 161 | pkg_size, 162 | installed_size, 163 | archive_size, 164 | location, 165 | pkg_time, 166 | }; 167 | 168 | // Set the matadata to the package 169 | let metadata = to_string(&rpm)?; 170 | package.set_metadata(metadata); 171 | 172 | Ok(rpm) 173 | } 174 | } 175 | 176 | fn flag_to_condition(flags: DependencyFlags) -> String { 177 | if flags.contains(DependencyFlags::GE) { 178 | "GE".to_owned() 179 | } else if flags.contains(DependencyFlags::LE) { 180 | "LE".to_owned() 181 | } else if flags.contains(DependencyFlags::EQUAL) { 182 | "EQ".to_owned() 183 | } else { 184 | "".to_owned() 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use std::fs::read; 191 | 192 | use insta::assert_debug_snapshot; 193 | 194 | use crate::package::tests::package_with_ver; 195 | use super::*; 196 | 197 | #[test] 198 | fn test_parser() { 199 | let package = package_with_ver("OpenBangla-Keyboard_2.0.0-fedora38.rpm", "2.0.0"); 200 | let data = read("data/OpenBangla-Keyboard_2.0.0-fedora38.rpm").unwrap(); 201 | package.set_package_data(data); 202 | let parsed = RPMPackage::from_package(&package).unwrap(); 203 | assert_debug_snapshot!(parsed); 204 | 205 | let package = package_with_ver("fastfetch-linux-amd64.rpm", "2.40.3"); 206 | let data = read("data/fastfetch-linux-amd64.rpm").unwrap(); 207 | package.set_package_data(data); 208 | let parsed = RPMPackage::from_package(&package).unwrap(); 209 | assert_debug_snapshot!(parsed); 210 | } 211 | 212 | #[test] 213 | #[should_panic] 214 | fn test_package_without_data() { 215 | let package = package_with_ver("OpenBangla-Keyboard_2.0.0-fedora38.rpm", "2.0.0"); 216 | 217 | RPMPackage::from_package(&package).unwrap(); 218 | } 219 | 220 | #[test] 221 | fn test_loading_from_metadata() { 222 | let package = package_with_ver("OpenBangla-Keyboard_2.0.0-fedora38.rpm", "2.0.0"); 223 | let data = read("data/OpenBangla-Keyboard_2.0.0-fedora38.rpm").unwrap(); 224 | package.set_package_data(data); 225 | let _ = RPMPackage::from_package(&package).unwrap(); 226 | 227 | // The package data should have been replaced by the metadata 228 | assert!(matches!(package.data(), Data::Metadata(_))); 229 | 230 | let _ = RPMPackage::from_package(&package).unwrap(); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/rpm/routes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, anyhow}; 2 | use axum::{ 3 | Router, 4 | body::Body, 5 | extract::{Path, State}, 6 | response::IntoResponse, 7 | routing::get, 8 | }; 9 | use axum_extra::{headers::UserAgent, typed_header::TypedHeader}; 10 | use zstd::encode_all; 11 | 12 | use crate::{ 13 | REQWEST, 14 | error::AppError, 15 | repository::Repository, 16 | rpm::{index::get_repomd_index, package::RPMPackage}, 17 | state::AppState, 18 | }; 19 | 20 | use super::index::{get_filelists_index, get_other_index, get_primary_index}; 21 | 22 | #[tracing::instrument(name = "RPM Index", skip_all, fields(agent = agent.as_str()))] 23 | async fn index( 24 | State(state): State, 25 | Path((owner, repo, file)): Path<(String, String, String)>, 26 | TypedHeader(agent): TypedHeader, 27 | ) -> Result, AppError> { 28 | let mut repo = Repository::from_github(owner, repo, &state).await; 29 | let packages: Vec = repo 30 | .select_package_rpm(agent.as_str()) 31 | .await? 32 | .into_iter() 33 | .map(|p| RPMPackage::from_package(&p).context(format!("Error while parsing package into RPMPackage: {p:?}"))) 34 | .collect::, _>>()?; 35 | 36 | repo.save_package_metadata().await; 37 | 38 | match file.as_str() { 39 | "repomd.xml" => Ok(get_repomd_index(&packages).into_bytes()), 40 | "repomd.xml.asc" => { 41 | let metadata = get_repomd_index(&packages); 42 | let signature = state.detached_sign_metadata(&metadata)?; 43 | Ok(signature) 44 | } 45 | "repomd.xml.key" => Ok(state.armored_public_key()), 46 | "primary.xml.zst" => Ok(encode_all(get_primary_index(&packages).as_bytes(), 0)?), 47 | "filelists.xml.zst" => Ok(encode_all(get_filelists_index(&packages).as_bytes(), 0)?), 48 | "other.xml.zst" => Ok(encode_all(get_other_index(&packages).as_bytes(), 0)?), 49 | file => Err(anyhow!("Unknown file requested: {file}").into()), 50 | } 51 | } 52 | 53 | #[tracing::instrument(name = "RPM Package proxy", skip_all)] 54 | async fn package( 55 | Path((owner, repo, ver, file)): Path<(String, String, String, String)>, 56 | ) -> Result { 57 | let url = format!("https://github.com/{owner}/{repo}/releases/download/{ver}/{file}"); 58 | let res = REQWEST 59 | .get(url) 60 | .send() 61 | .await 62 | .context("Error occurred while proxying package")?; 63 | let stream = res.bytes_stream(); 64 | let stream = Body::from_stream(stream); 65 | 66 | Ok(stream) 67 | } 68 | 69 | pub fn rpm_routes() -> Router { 70 | Router::new() 71 | .route("/github/{owner}/{repo}/repodata/{file}", get(index)) 72 | .route("/github/{owner}/{repo}/package/{ver}/{file}", get(package)) 73 | } 74 | -------------------------------------------------------------------------------- /src/rpm/snapshots/packhub__rpm__index__tests__multiple_arch-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rpm/index.rs 3 | expression: get_other_index(&packages) 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/rpm/snapshots/packhub__rpm__index__tests__multiple_arch-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rpm/index.rs 3 | expression: get_repomd_index(&packages) 4 | --- 5 | 6 | 7 | 0 8 | 9 | e1e935dbb4860aa4bf069feb8047dbb2cbd99fd12a20a6d9951a2fcb93b42a50 10 | 27352b51e815b6c80a3e610efb546e92d4162815f6a494cd3208f4a6ae2c3068 11 | 12 | 0 13 | 1879 14 | 16320 15 | 16 | 17 | 56e7abb7ffc03f2d059b7f603f44080ee3e9e576e4f2fbe605cda3e0dafa1a80 18 | 50ad11cb1bd00e19976d372043e7b2b6d1b4d603ff133e74e819607d7677ec30 19 | 20 | 0 21 | 1347 22 | 27366 23 | 24 | 25 | ddbe672908c8135dc346a4cc28b6024581d684440956d7fdab3b8fa57ad964cd 26 | b76d98af4e9faa195546993c02134bce159d508931fb2fa51ee0f0b18e9ee433 27 | 28 | 0 29 | 518 30 | 1314 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/rpm/snapshots/packhub__rpm__index__tests__rpm_indices-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rpm/index.rs 3 | expression: get_filelists_index(packages.clone()) 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | /usr/lib/.build-id 13 | 14 | 15 | 16 | /usr/lib/.build-id/45 17 | 18 | 19 | 20 | /usr/lib/.build-id/45/4f3de1c02f67096ca79f7edb9030f7b1b4d015 21 | 22 | 23 | 24 | /usr/lib/.build-id/b2 25 | 26 | 27 | 28 | /usr/lib/.build-id/b2/1aa073a506f8b918fda931c8d12912b5d164be 29 | 30 | 31 | 32 | /usr/share/applications/openbangla-keyboard.desktop 33 | 34 | 35 | 36 | /usr/share/ibus/component/openbangla.xml 37 | 38 | 39 | 40 | /usr/share/icons/hicolor/1024x1024/apps/openbangla-keyboard.png 41 | 42 | 43 | 44 | /usr/share/icons/hicolor/128x128/apps/openbangla-keyboard.png 45 | 46 | 47 | 48 | /usr/share/icons/hicolor/16x16/apps/openbangla-keyboard.png 49 | 50 | 51 | 52 | /usr/share/icons/hicolor/32x32/apps/openbangla-keyboard.png 53 | 54 | 55 | 56 | /usr/share/icons/hicolor/48x48/apps/openbangla-keyboard.png 57 | 58 | 59 | 60 | /usr/share/icons/hicolor/512x512/apps/openbangla-keyboard.png 61 | 62 | 63 | 64 | /usr/share/metainfo/io.github.openbangla.keyboard.metainfo.xml 65 | 66 | 67 | 68 | /usr/share/openbangla-keyboard 69 | 70 | 71 | 72 | /usr/share/openbangla-keyboard/data 73 | 74 | 75 | 76 | /usr/share/openbangla-keyboard/data/autocorrect.json 77 | 78 | 79 | 80 | /usr/share/openbangla-keyboard/data/dictionary.json 81 | 82 | 83 | 84 | /usr/share/openbangla-keyboard/data/regex.json 85 | 86 | 87 | 88 | /usr/share/openbangla-keyboard/data/suffix.json 89 | 90 | 91 | 92 | /usr/share/openbangla-keyboard/ibus-openbangla 93 | 94 | 95 | 96 | /usr/share/openbangla-keyboard/icons 97 | 98 | 99 | 100 | /usr/share/openbangla-keyboard/icons/OpenBangla-Keyboard.png 101 | 102 | 103 | 104 | /usr/share/openbangla-keyboard/layouts 105 | 106 | 107 | 108 | /usr/share/openbangla-keyboard/layouts/Avro_Easy.json 109 | 110 | 111 | 112 | /usr/share/openbangla-keyboard/layouts/Borno.json 113 | 114 | 115 | 116 | /usr/share/openbangla-keyboard/layouts/Munir_Optima.json 117 | 118 | 119 | 120 | /usr/share/openbangla-keyboard/layouts/National_Jatiya.json 121 | 122 | 123 | 124 | /usr/share/openbangla-keyboard/layouts/Probhat.json 125 | 126 | 127 | 128 | /usr/share/openbangla-keyboard/layouts/avrophonetic.json 129 | 130 | 131 | 132 | /usr/share/openbangla-keyboard/openbangla-gui 133 | 134 | 135 | 136 | /usr/share/pixmaps/openbangla-keyboard.png 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/rpm/snapshots/packhub__rpm__index__tests__rpm_indices-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rpm/index.rs 3 | expression: get_other_index(packages) 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/rpm/snapshots/packhub__rpm__index__tests__rpm_indices-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rpm/index.rs 3 | expression: get_repomd_index(&packages) 4 | --- 5 | 6 | 7 | 1699461612 8 | 9 | acd512d27f4a86955aaf017633a86fefc6e655d98f09cd29dc0fb0d1bf6bb10f 10 | 6626d360f5285336ecddc887b2f93a2ae11ea96be2326e63fc74a6c297f3f9bc 11 | 12 | 1699461612 13 | 1380 14 | 6204 15 | 16 | 17 | e1f5a56b0524fd542e9bc028c73fbcc313f50cdd6c23ba6209cd2361d28859d6 18 | 5b06da6230b55ba1746bb5d04be47271145b0fff9ded8aa70dcb1a5d01df9066 19 | 20 | 1699461612 21 | 704 22 | 2737 23 | 24 | 25 | 6a131fe89575f354d8681f3fe8873a42b1ae97fec79d03a43c123e980713e5e8 26 | 267be24ed5d9366ec2f513166409b1daa9ac0e781f7477374aada48db45cb947 27 | 28 | 1699461612 29 | 248 30 | 306 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/rpm/snapshots/packhub__rpm__index__tests__rpm_indices.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rpm/index.rs 3 | expression: get_primary_index(&packages) 4 | --- 5 | 6 | 7 | 8 | 9 | openbangla-keyboard 10 | x86_64 11 | 12 | 4ea19432c75d1b7ddc6dc2adbb10301fea142913834decd0455e1249cbaf3c14 13 | 14 | OpenSource Bengali input method 15 | 16 | 17 | OpenBangla Keyboard is an OpenSource, Unicode compliant Bengali Input Method for GNU/Linux systems. It's a full fledged Bengali input method with typing automation tools, includes many famous typing methods such as Avro Phonetic, Probhat, Munir Optima, National (Jatiya) etc. 18 | 19 | Most of the features of Avro Keyboard are present in OpenBangla Keyboard. So Avro Keyboard users will feel at home with OpenBangla Keyboard in Linux. 20 | 21 | 22 | 23 | https://openbangla.github.io/ 24 | 25 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /src/script.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use askama::Template; 3 | use axum::{Router, extract::Path, routing::get}; 4 | 5 | use crate::{error::AppError, state::AppState}; 6 | 7 | #[derive(Template)] 8 | #[template(path = "apt-script.sh", escape = "none")] 9 | struct AptScript<'a> { 10 | host: &'a str, 11 | distro: &'a str, 12 | owner: &'a str, 13 | repo: &'a str, 14 | } 15 | 16 | fn generate_apt_script(distro: &str, owner: &str, repo: &str) -> String { 17 | let host = dotenvy::var("PACKHUB_DOMAIN").unwrap(); 18 | let script = AptScript { 19 | host: &host, 20 | distro, 21 | owner, 22 | repo, 23 | }; 24 | script.render().unwrap() 25 | } 26 | 27 | #[derive(Template)] 28 | #[template(path = "rpm-script.sh", escape = "none")] 29 | struct RPMScript<'a> { 30 | host: &'a str, 31 | owner: &'a str, 32 | repo: &'a str, 33 | mgr: &'a str, 34 | } 35 | 36 | fn generate_rpm_script(owner: &str, repo: &str, mgr: &str) -> String { 37 | let host = dotenvy::var("PACKHUB_DOMAIN").unwrap(); 38 | let script = RPMScript { 39 | host: &host, 40 | owner, 41 | repo, 42 | mgr, 43 | }; 44 | script.render().unwrap() 45 | } 46 | 47 | async fn script_handler( 48 | Path((distro, owner, repo)): Path<(String, String, String)>, 49 | ) -> Result { 50 | match distro.as_str() { 51 | "ubuntu" | "debian" => Ok(generate_apt_script(&distro, &owner, &repo)), 52 | "yum" => Ok(generate_rpm_script(&owner, &repo, "yum.repos.d")), 53 | "zypp" => Ok(generate_rpm_script(&owner, &repo, "zypp/repos.d")), 54 | _ => Err(anyhow!("Script Generation: Unsupported distro: {}", distro).into()), 55 | } 56 | } 57 | 58 | pub fn script_routes() -> Router { 59 | Router::new().route("/{distro}/github/{owner}/{repo}", get(script_handler)) 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use insta::assert_snapshot; 65 | 66 | use super::*; 67 | 68 | #[test] 69 | fn test_script_generation_apt() { 70 | let apt_script = generate_apt_script("ubuntu", "OpenBangla", "OpenBangla-Keyboard"); 71 | assert_snapshot!(apt_script); 72 | } 73 | 74 | #[test] 75 | fn test_script_generation_rpm() { 76 | let yum = generate_rpm_script("OpenBangla", "OpenBangla-Keyboard", "yum.repos.d"); 77 | assert_snapshot!(yum); 78 | 79 | let zypp = generate_rpm_script("OpenBangla", "OpenBangla-Keyboard", "zypp/repos.d"); 80 | assert_snapshot!(zypp); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/selector.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the logic to select the best package for a given distribution. 2 | //! It filters the packages based on the distribution and version, and returns the best match. 3 | 4 | use std::collections::HashMap; 5 | 6 | use crate::{package::Package, utils::Dist}; 7 | 8 | pub(crate) fn select_packages(from: &[Package], dist: Dist) -> Vec<&Package> { 9 | let mut packages = Vec::new(); 10 | 11 | // Filter out the packages that are not for the distribution. 12 | for package in from { 13 | if package.ty().matches_distribution(&dist) { 14 | packages.push(package); 15 | } 16 | } 17 | 18 | // Find matches for the distribution. 19 | let mut selective = Vec::new(); 20 | 21 | // Loosely match the distribution (without regarding the distribution version). 22 | for package in packages.iter() { 23 | if let Some(pack_dist) = package.distribution() { 24 | if dist.matches_distribution(pack_dist) { 25 | selective.push(*package); 26 | } 27 | } 28 | } 29 | 30 | // Search for the exact distribution version match. 31 | // When there is no exact version match 32 | // but lower version match is available, 33 | // then we need to select the closest version. 34 | if !selective.is_empty() { 35 | // Group packages by name. 36 | let mut packages_by_name: HashMap<&str, Vec<&Package>> = HashMap::new(); 37 | for package in selective.iter() { 38 | if let Some(name) = package.name() { 39 | packages_by_name.entry(name).or_default().push(*package); 40 | } 41 | } 42 | 43 | // Sort the packages by target distribution version and cut off greater versions. 44 | for (_, packages) in packages_by_name.iter_mut() { 45 | packages.sort_by(|a, b| b.distribution().unwrap().cmp(a.distribution().unwrap())); 46 | packages.retain(|i| dist >= *i.distribution().unwrap()); 47 | } 48 | 49 | // Group by name and architecture 50 | let mut grouped_by_name_and_arch: HashMap<_, Vec<_>> = HashMap::new(); 51 | for (name, packages) in packages_by_name.iter() { 52 | for package in packages.iter() { 53 | let arch = package.architecture(); 54 | grouped_by_name_and_arch 55 | .entry((name, arch)) 56 | .or_default() 57 | .push(*package); 58 | } 59 | } 60 | 61 | // Take the first package from each group. 62 | // This will give us the packages that are closest to the target distribution. 63 | let merged = grouped_by_name_and_arch 64 | .into_values() 65 | .map(|v| v[0]) 66 | .collect::>(); 67 | 68 | // If we have exact or relatively matched packages, then return them. 69 | if !merged.is_empty() { 70 | return merged; 71 | } 72 | 73 | // We have no exact or relative match, so return the selective packages. 74 | return selective; 75 | } 76 | 77 | packages 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | use crate::package::tests::package; 84 | 85 | fn openbangla_keyboard_packages() -> Vec { 86 | [ 87 | // TODO: Package::detect_package("OpenBangla-Keyboard_2.0.0-archlinux.pkg.tar.zst", String::new()).unwrap(), 88 | package("OpenBangla-Keyboard_2.0.0-debian10-buster.deb"), 89 | package("OpenBangla-Keyboard_2.0.0-debian11.deb"), 90 | package("OpenBangla-Keyboard_2.0.0-debian9-stretch.deb"), 91 | package("OpenBangla-Keyboard_2.0.0-fedora29.rpm"), 92 | package("OpenBangla-Keyboard_2.0.0-fedora30.rpm"), 93 | package("OpenBangla-Keyboard_2.0.0-fedora31.rpm"), 94 | package("OpenBangla-Keyboard_2.0.0-fedora32.rpm"), 95 | package("OpenBangla-Keyboard_2.0.0-fedora33.rpm"), 96 | package("OpenBangla-Keyboard_2.0.0-fedora34.rpm"), 97 | package("OpenBangla-Keyboard_2.0.0-fedora35.rpm"), 98 | package("OpenBangla-Keyboard_2.0.0-fedora36.rpm"), 99 | package("OpenBangla-Keyboard_2.0.0-fedora37.rpm"), 100 | package("OpenBangla-Keyboard_2.0.0-fedora38.rpm"), 101 | package("OpenBangla-Keyboard_2.0.0-ubuntu18.04.deb"), 102 | package("OpenBangla-Keyboard_2.0.0-ubuntu19.10.deb"), 103 | package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb"), 104 | package("OpenBangla-Keyboard_2.0.0-ubuntu21.04.deb"), 105 | package("OpenBangla-Keyboard_2.0.0-ubuntu22.04.deb"), 106 | ] 107 | .into() 108 | } 109 | 110 | fn multiple_packages() -> Vec { 111 | [ 112 | package("fcitx-openbangla_3.0.0.deb"), 113 | package("ibus-openbangla_3.0.0.deb"), 114 | package("fcitx-openbangla_3.0.0-fedora.rpm"), 115 | package("ibus-openbangla_3.0.0-fedora.rpm"), 116 | package("ibus-openbangla_3.0.0-opensuse-tumbleweed.rpm"), 117 | package("fcitx-openbangla_3.0.0-opensuse-tumbleweed.rpm"), 118 | ] 119 | .into() 120 | } 121 | 122 | // We need to sort the packages to make the test deterministic. 123 | // Because `select_packages` sorts the packages by the distribution 124 | // which can change the order of the packages when multiple packages 125 | // are present. So we need to sort the packages by their file name. 126 | fn sort<'a>(mut v: Vec<&'a Package>) -> Vec<&'a Package> { 127 | v.sort(); 128 | v 129 | } 130 | 131 | #[test] 132 | fn test_package_selection_ubuntu() { 133 | let packages: Vec = openbangla_keyboard_packages(); 134 | 135 | assert_eq!( 136 | select_packages(&packages, Dist::ubuntu("18.04")), 137 | vec![&package("OpenBangla-Keyboard_2.0.0-ubuntu18.04.deb")] 138 | ); 139 | assert_eq!( 140 | select_packages(&packages, Dist::ubuntu("20.04")), 141 | vec![&package("OpenBangla-Keyboard_2.0.0-ubuntu20.04.deb")] 142 | ); 143 | assert_eq!( 144 | select_packages(&packages, Dist::ubuntu("22.04")), 145 | vec![&package("OpenBangla-Keyboard_2.0.0-ubuntu22.04.deb")] 146 | ); 147 | } 148 | 149 | #[test] 150 | fn test_package_selection_fedora() { 151 | let packages: Vec = openbangla_keyboard_packages(); 152 | 153 | assert_eq!( 154 | select_packages(&packages, Dist::fedora("38")), 155 | vec![&package("OpenBangla-Keyboard_2.0.0-fedora38.rpm")] 156 | ); 157 | } 158 | 159 | #[test] 160 | fn test_package_selection_debian() { 161 | let packages: Vec = openbangla_keyboard_packages(); 162 | 163 | assert_eq!( 164 | select_packages(&packages, Dist::debian("11")), 165 | vec![&package("OpenBangla-Keyboard_2.0.0-debian11.deb")] 166 | ); 167 | } 168 | 169 | #[test] 170 | fn test_multiple_package_selection() { 171 | let packages = multiple_packages(); 172 | 173 | assert_eq!( 174 | sort(select_packages(&packages, Dist::ubuntu("22.04"))), 175 | vec![ 176 | &package("fcitx-openbangla_3.0.0.deb"), 177 | &package("ibus-openbangla_3.0.0.deb") 178 | ] 179 | ); 180 | 181 | assert_eq!( 182 | sort(select_packages(&packages, Dist::fedora("39"))), 183 | vec![ 184 | &package("fcitx-openbangla_3.0.0-fedora.rpm"), 185 | &package("ibus-openbangla_3.0.0-fedora.rpm") 186 | ] 187 | ); 188 | 189 | assert_eq!( 190 | sort(select_packages(&packages, Dist::Tumbleweed)), 191 | vec![ 192 | &package("fcitx-openbangla_3.0.0-opensuse-tumbleweed.rpm"), 193 | &package("ibus-openbangla_3.0.0-opensuse-tumbleweed.rpm") 194 | ] 195 | ); 196 | } 197 | 198 | #[test] 199 | fn test_package_selection_closest() { 200 | let packages: Vec = openbangla_keyboard_packages(); 201 | 202 | assert_eq!( 203 | select_packages(&packages, Dist::fedora("41")), 204 | vec![&package("OpenBangla-Keyboard_2.0.0-fedora38.rpm")] 205 | ); 206 | assert_eq!( 207 | select_packages(&packages, Dist::ubuntu("24.04")), 208 | vec![&package("OpenBangla-Keyboard_2.0.0-ubuntu22.04.deb")] 209 | ); 210 | 211 | let packages = [ 212 | package("flameshot-12.1.0-1-lp15.2.x86_64.rpm"), 213 | package("flameshot-12.1.0-1.debian-10.amd64.deb"), 214 | package("flameshot-12.1.0-1.debian-10.arm64.deb"), 215 | package("flameshot-12.1.0-1.debian-10.armhf.deb"), 216 | package("flameshot-12.1.0-1.debian-11.amd64.deb"), 217 | package("flameshot-12.1.0-1.debian-11.arm64.deb"), 218 | package("flameshot-12.1.0-1.debian-11.armhf.deb"), 219 | package("flameshot-12.1.0-1.fc35.x86_64.rpm"), 220 | package("flameshot-12.1.0-1.fc36.x86_64.rpm"), 221 | package("flameshot-12.1.0-1.ubuntu-20.04.amd64.deb"), 222 | package("flameshot-12.1.0-1.ubuntu-22.04.amd64.deb"), 223 | ]; 224 | 225 | assert_eq!( 226 | select_packages(&packages, Dist::fedora("39")), 227 | vec![&package("flameshot-12.1.0-1.fc36.x86_64.rpm")] 228 | ); 229 | 230 | assert_eq!( 231 | select_packages(&packages, Dist::ubuntu("21.04")), 232 | vec![&package("flameshot-12.1.0-1.ubuntu-20.04.amd64.deb")] 233 | ); 234 | 235 | assert_eq!( 236 | select_packages(&packages, Dist::ubuntu("24.04")), 237 | vec![&package("flameshot-12.1.0-1.ubuntu-22.04.amd64.deb")] 238 | ); 239 | 240 | assert_eq!( 241 | sort(select_packages(&packages, Dist::debian("12"))), 242 | vec![ 243 | &package("flameshot-12.1.0-1.debian-11.amd64.deb"), 244 | &package("flameshot-12.1.0-1.debian-11.arm64.deb"), 245 | &package("flameshot-12.1.0-1.debian-11.armhf.deb") 246 | ] 247 | ); 248 | } 249 | 250 | #[test] 251 | fn test_package_selection_without_dist() { 252 | let packages = [package("caprine_2.60.3_amd64.deb")]; 253 | 254 | assert_eq!( 255 | select_packages(&packages, Dist::ubuntu("24.04")), 256 | vec![&package("caprine_2.60.3_amd64.deb")] 257 | ); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/snapshots/packhub__script__tests__script_generation_apt.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/script.rs 3 | expression: apt_script 4 | --- 5 | #!/bin/sh 6 | 7 | echo "Welcome to package key and repository setup script for OpenBangla-Keyboard" 8 | echo "This script will add the repository key and repository to your system." 9 | echo "Please make sure you have sudo access to run this script." 10 | echo 11 | echo "Downloading and installing the repository key..." 12 | wget -qO- http://localhost:3000/v1/keys/packhub.gpg | sudo tee /etc/apt/keyrings/packhub.gpg > /dev/null 13 | echo 14 | echo "Adding the repository to your system..." 15 | echo "deb [signed-by=/etc/apt/keyrings/packhub.gpg] http://localhost:3000/v1/apt/ubuntu/github/OpenBangla/OpenBangla-Keyboard stable main" | sudo tee /etc/apt/sources.list.d/OpenBangla-Keyboard.list > /dev/null 16 | echo 17 | echo "Updating package lists..." 18 | sudo apt-get update 19 | -------------------------------------------------------------------------------- /src/snapshots/packhub__script__tests__script_generation_rpm-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/script.rs 3 | expression: zypp 4 | --- 5 | #!/bin/sh 6 | 7 | echo "Welcome to package key and repository setup script for OpenBangla-Keyboard" 8 | echo "This script will add the repository key and repository to your system." 9 | echo "Please make sure you have sudo access to run this script." 10 | 11 | echo -e "[OpenBangla-Keyboard]\nname=OpenBangla-Keyboard\nbaseurl=http://localhost:3000/v1/rpm/github/OpenBangla/OpenBangla-Keyboard\nenabled=1\ngpgcheck=0\nrepo_gpgcheck=1\ngpgkey=http://localhost:3000/v1/keys/packhub.asc" | sudo tee /etc/zypp/repos.d/OpenBangla-Keyboard.repo > /dev/null 12 | 13 | echo 14 | echo "Repository has been added to your system." 15 | echo "Please update your package lists to start using the repository." 16 | echo "Use 'dnf update' or 'yum update' or 'zypper refresh' depending on your package manager." 17 | -------------------------------------------------------------------------------- /src/snapshots/packhub__script__tests__script_generation_rpm.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/script.rs 3 | expression: yum 4 | --- 5 | #!/bin/sh 6 | 7 | echo "Welcome to package key and repository setup script for OpenBangla-Keyboard" 8 | echo "This script will add the repository key and repository to your system." 9 | echo "Please make sure you have sudo access to run this script." 10 | 11 | echo -e "[OpenBangla-Keyboard]\nname=OpenBangla-Keyboard\nbaseurl=http://localhost:3000/v1/rpm/github/OpenBangla/OpenBangla-Keyboard\nenabled=1\ngpgcheck=0\nrepo_gpgcheck=1\ngpgkey=http://localhost:3000/v1/keys/packhub.asc" | sudo tee /etc/yum.repos.d/OpenBangla-Keyboard.repo > /dev/null 12 | 13 | echo 14 | echo "Repository has been added to your system." 15 | echo "Please update your package lists to start using the repository." 16 | echo "Use 'dnf update' or 'yum update' or 'zypper refresh' depending on your package manager." 17 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use dotenvy::var; 5 | use mongodb::Client; 6 | use octocrab::{Octocrab, OctocrabBuilder}; 7 | use sequoia_openpgp::{Cert, crypto::Password, serialize::SerializeInto}; 8 | 9 | use crate::pgp::{ 10 | clearsign_metadata, detached_sign_metadata, generate_and_save_keys, load_cert_from_file, 11 | }; 12 | 13 | #[derive(Clone)] 14 | pub struct AppState { 15 | state: Arc, 16 | } 17 | 18 | struct InnerState { 19 | db: Client, 20 | cert: Cert, 21 | github: Octocrab, 22 | passphrase: Password, 23 | } 24 | 25 | impl AppState { 26 | pub async fn initialize(generate_keys: bool) -> Self { 27 | let uri = format!( 28 | "mongodb://{}:{}@{}:27017", 29 | var("PACKHUB_DB_USER").unwrap(), 30 | var("PACKHUB_DB_PASSWORD").unwrap(), 31 | var("PACKHUB_DB_HOST").unwrap() 32 | ); 33 | 34 | let client = Client::with_uri_str(uri).await.unwrap(); 35 | let passphrase = var("PACKHUB_SIGN_PASSPHRASE").unwrap().into(); 36 | 37 | let cert = if generate_keys { 38 | generate_and_save_keys(&passphrase).unwrap() 39 | } else { 40 | load_cert_from_file().unwrap() 41 | }; 42 | 43 | let pat = var("PACKHUB_GITHUB_PAT").unwrap(); 44 | 45 | let github = if pat != "" { 46 | OctocrabBuilder::default() 47 | .personal_token(var("PACKHUB_GITHUB_PAT").unwrap()) 48 | .build() 49 | .unwrap() 50 | } else { 51 | OctocrabBuilder::default().build().unwrap() 52 | }; 53 | 54 | Self { 55 | state: Arc::new(InnerState { 56 | db: client, 57 | cert, 58 | github, 59 | passphrase, 60 | }), 61 | } 62 | } 63 | 64 | /// Get a reference to the MongoDB client. 65 | pub fn db(&self) -> &Client { 66 | &self.state.db 67 | } 68 | 69 | /// Get a reference to the GitHub client. 70 | pub fn github(&self) -> &Octocrab { 71 | &self.state.github 72 | } 73 | 74 | pub fn clearsign_metadata(&self, data: &str) -> Result> { 75 | clearsign_metadata(data, &self.state.cert, &self.state.passphrase) 76 | } 77 | 78 | pub fn detached_sign_metadata(&self, data: &str) -> Result> { 79 | detached_sign_metadata(data, &self.state.cert, &self.state.passphrase) 80 | } 81 | 82 | pub fn armored_public_key(&self) -> Vec { 83 | self.state.cert.armored().to_vec().unwrap() 84 | } 85 | 86 | pub fn dearmored_public_key(&self) -> Vec { 87 | self.state.cert.to_vec().unwrap() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, ops::Add, str::FromStr}; 2 | 3 | use anyhow::Result; 4 | use lenient_semver::parse; 5 | use semver::Version; 6 | use sha1::digest::{Digest, OutputSizeUser, generic_array::ArrayLength}; 7 | 8 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] 9 | pub enum Dist { 10 | Ubuntu(Option), 11 | Debian(Option), 12 | Fedora(Option), 13 | Tumbleweed, 14 | Leap(Option), 15 | } 16 | 17 | impl Dist { 18 | /// Check if it matches the `dist` without regarding its version. 19 | /// 20 | /// A loose check than `==`. 21 | pub fn matches_distribution(&self, dist: &Dist) -> bool { 22 | match self { 23 | Dist::Debian(_) => matches!(dist, Dist::Debian(_)), 24 | Dist::Ubuntu(_) => matches!(dist, Dist::Ubuntu(_)), 25 | Dist::Fedora(_) => matches!(dist, Dist::Fedora(_)), 26 | Dist::Tumbleweed => matches!(dist, Dist::Tumbleweed), 27 | Dist::Leap(_) => matches!(dist, Dist::Leap(_)), 28 | } 29 | } 30 | 31 | pub fn set_version(&mut self, version: Option<&str>) { 32 | match self { 33 | Dist::Debian(ver) => *ver = version.and_then(|v| parse(v).ok()), 34 | Dist::Ubuntu(ver) => *ver = version.and_then(|v| parse(v).ok()), 35 | Dist::Fedora(ver) => *ver = version.and_then(|v| parse(v).ok()), 36 | Dist::Tumbleweed => {} 37 | Dist::Leap(ver) => *ver = version.and_then(|v| parse(v).ok()), 38 | } 39 | } 40 | 41 | pub fn ubuntu(version: &str) -> Self { 42 | Dist::Ubuntu(parse(version).ok()) 43 | } 44 | 45 | pub fn debian(version: &str) -> Self { 46 | Dist::Debian(parse(version).ok()) 47 | } 48 | 49 | pub fn fedora(version: &str) -> Self { 50 | Dist::Fedora(parse(version).ok()) 51 | } 52 | 53 | pub fn leap(version: &str) -> Self { 54 | Dist::Leap(parse(version).ok()) 55 | } 56 | } 57 | 58 | #[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] 59 | pub enum Arch { 60 | #[default] 61 | Amd64, 62 | Arm64, 63 | Armhf, // Hard Float ABI ARM. Compatible with armv7 and armv6 64 | Armv7, 65 | Aarch64, 66 | PPC64le, 67 | RiscV64, 68 | S390x, 69 | } 70 | 71 | impl FromStr for Arch { 72 | type Err = (); 73 | 74 | fn from_str(s: &str) -> Result { 75 | match s { 76 | "amd64" => Ok(Arch::Amd64), 77 | "x86_64" => Ok(Arch::Amd64), 78 | "aarch64" => Ok(Arch::Aarch64), 79 | "arm64" => Ok(Arch::Arm64), 80 | "armhf" => Ok(Arch::Armhf), 81 | "armv6l" => Ok(Arch::Armhf), 82 | "armv7" => Ok(Arch::Armv7), 83 | "armv7l" => Ok(Arch::Armv7), 84 | "ppc64le" => Ok(Arch::PPC64le), 85 | "riscv64" => Ok(Arch::RiscV64), 86 | "s390x" => Ok(Arch::S390x), 87 | _ => Err(()), 88 | } 89 | } 90 | } 91 | 92 | // Currently follows the naming convention of Debian 93 | // https://www.debian.org/ports/#portlist-released 94 | impl Display for Arch { 95 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 | match self { 97 | Arch::Amd64 => write!(f, "amd64"), 98 | Arch::Arm64 => write!(f, "arm64"), 99 | Arch::Armhf => write!(f, "armhf"), 100 | Arch::Armv7 => write!(f, "armhf"), 101 | Arch::Aarch64 => write!(f, "arm64"), 102 | Arch::PPC64le => write!(f, "ppc64el"), 103 | Arch::RiscV64 => write!(f, "riscv64"), 104 | Arch::S390x => write!(f, "s390x"), 105 | } 106 | } 107 | } 108 | 109 | #[derive(Debug, PartialEq, Clone)] 110 | pub enum Type { 111 | Deb, 112 | Rpm, 113 | } 114 | 115 | impl Type { 116 | pub fn matches_distribution(&self, dist: &Dist) -> bool { 117 | match self { 118 | Type::Deb => matches!(dist, Dist::Debian(_) | Dist::Ubuntu(_)), 119 | Type::Rpm => matches!(dist, Dist::Fedora(_) | Dist::Tumbleweed), 120 | } 121 | } 122 | } 123 | 124 | pub fn hashsum(data: &[u8]) -> String 125 | where 126 | ::OutputSize: Add, 127 | <::OutputSize as Add>::Output: ArrayLength, 128 | { 129 | format!("{:x}", T::digest(data)) 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | 136 | #[test] 137 | fn test_type_matches_distribution() { 138 | assert!(Type::Deb.matches_distribution(&Dist::Debian(None))); 139 | assert!(Type::Deb.matches_distribution(&Dist::Ubuntu(None))); 140 | assert!(!Type::Deb.matches_distribution(&Dist::Fedora(None))); 141 | assert!(Type::Rpm.matches_distribution(&Dist::Fedora(None))); 142 | assert!(Type::Rpm.matches_distribution(&Dist::Tumbleweed)); 143 | } 144 | 145 | #[test] 146 | fn test_dist_matches() { 147 | assert!(Dist::Ubuntu(None).matches_distribution(&Dist::ubuntu("24.04"))); 148 | assert!(!Dist::Debian(None).matches_distribution(&Dist::ubuntu("24.04"))); 149 | } 150 | 151 | #[test] 152 | fn test_dist_version_comparison() { 153 | let ver1 = Dist::ubuntu("24.04"); 154 | let ver2 = Dist::ubuntu("24.10"); 155 | let ver0 = Dist::Ubuntu(None); 156 | 157 | assert!(ver1 < ver2); 158 | assert!(ver2 > ver1); 159 | assert!(ver1 > ver0); 160 | assert!(ver0 < ver1); 161 | 162 | let ver3 = Dist::fedora("38"); 163 | let ver4 = Dist::fedora("41"); 164 | let ver0 = Dist::Fedora(None); 165 | 166 | assert!(ver3 < ver4); 167 | assert!(ver4 > ver3); 168 | assert!(ver3 > ver0); 169 | assert!(ver0 < ver3); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /templates/Packages: -------------------------------------------------------------------------------- 1 | {% for package in packages -%} 2 | {{ package.control }} 3 | MD5sum: {{ package.md5 }} 4 | SHA1: {{ package.sha1 }} 5 | SHA256: {{ package.sha256 }} 6 | SHA512: {{ package.sha512 }} 7 | Size: {{ package.size }} 8 | Filename: {{ package.filename }} 9 | 10 | {% endfor -%} -------------------------------------------------------------------------------- /templates/Release: -------------------------------------------------------------------------------- 1 | Origin: {{ origin }} 2 | Label: {{ label }} 3 | Suite: stable 4 | Codename: stable 5 | Date: {{ date }} 6 | Architectures: amd64 7 | Components: main 8 | Description: Generated by packhub 9 | MD5Sum: 10 | {%- for file in files %} 11 | {{ file.md5 }} {{ file.size }} {{ file.path }} 12 | {%- endfor %} 13 | SHA1: 14 | {%- for file in files %} 15 | {{ file.sha1 }} {{ file.size }} {{ file.path }} 16 | {%- endfor %} 17 | SHA256: 18 | {%- for file in files %} 19 | {{ file.sha256 }} {{ file.size }} {{ file.path }} 20 | {%- endfor %} 21 | SHA512: 22 | {%- for file in files %} 23 | {{ file.sha512 }} {{ file.size }} {{ file.path }} 24 | {%- endfor %} 25 | -------------------------------------------------------------------------------- /templates/apt-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Welcome to package key and repository setup script for {{repo}}" 4 | echo "This script will add the repository key and repository to your system." 5 | echo "Please make sure you have sudo access to run this script." 6 | echo 7 | echo "Downloading and installing the repository key..." 8 | wget -qO- {{host}}/v1/keys/packhub.gpg | sudo tee /etc/apt/keyrings/packhub.gpg > /dev/null 9 | echo 10 | echo "Adding the repository to your system..." 11 | echo "deb [signed-by=/etc/apt/keyrings/packhub.gpg] {{host}}/v1/apt/{{distro}}/github/{{owner}}/{{repo}} stable main" | sudo tee /etc/apt/sources.list.d/{{repo}}.list > /dev/null 12 | echo 13 | echo "Updating package lists..." 14 | sudo apt-get update 15 | -------------------------------------------------------------------------------- /templates/filelists.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for package in packages %} 4 | 5 | 6 | {% for file in package.files %} 7 | {% if file.dir %} 8 | {{ file.path }} 9 | {% else %} 10 | {{ file.path }} 11 | {% endif %} 12 | {% endfor %} 13 | 14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /templates/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for package in packages %} 4 | 5 | 6 | 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /templates/primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for package in packages %} 4 | 5 | {{ package.name }} 6 | {{ package.arch }} 7 | 8 | {{ package.sha256 }} 9 | {% if let Some(summary) = package.summary %} 10 | {{ summary }} 11 | {% endif %} 12 | {% if let Some(description) = package.description %} 13 | {{ description }} 14 | {% endif %} 15 | 16 | {% if let Some(url) = package.url %} 17 | {{ url }} 18 | {% endif %} 19 | 59 | {% endfor %} 60 | -------------------------------------------------------------------------------- /templates/repomd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ timestamp }} 4 | 5 | {{ primary.sha256 }} 6 | {{ primary. open_sha256 }} 7 | 8 | {{ timestamp }} 9 | {{ primary.size }} 10 | {{ primary.open_size }} 11 | 12 | 13 | {{ filelists.sha256 }} 14 | {{ filelists.open_sha256 }} 15 | 16 | {{ timestamp }} 17 | {{ filelists.size }} 18 | {{ filelists.open_size }} 19 | 20 | 21 | {{ other.sha256 }} 22 | {{ other.open_sha256}} 23 | 24 | {{ timestamp }} 25 | {{ other.size }} 26 | {{ other.open_size }} 27 | 28 | 29 | -------------------------------------------------------------------------------- /templates/rpm-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Welcome to package key and repository setup script for {{repo}}" 4 | echo "This script will add the repository key and repository to your system." 5 | echo "Please make sure you have sudo access to run this script." 6 | 7 | echo -e "[{{repo}}]\nname={{repo}}\nbaseurl={{host}}/v1/rpm/github/{{owner}}/{{repo}}\nenabled=1\ngpgcheck=0\nrepo_gpgcheck=1\ngpgkey={{host}}/v1/keys/packhub.asc" | sudo tee /etc/{{mgr}}/{{repo}}.repo > /dev/null 8 | 9 | echo 10 | echo "Repository has been added to your system." 11 | echo "Please update your package lists to start using the repository." 12 | echo "Use 'dnf update' or 'yum update' or 'zypper refresh' depending on your package manager." 13 | --------------------------------------------------------------------------------