├── .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 | [](https://github.com/mominul/packhub/actions?query=branch%3Amain)
3 | [](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html)
4 | [](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 |
--------------------------------------------------------------------------------
/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 |
26 |
27 |
28 |
29 |
30 | GPLv3
31 |
32 |
33 | OpenBangla Team
34 |
35 |
36 | unknown
37 |
38 |
39 | 3aebfc71f602
40 |
41 |
42 | openbangla-keyboard-2.0.0-1.fc38.src.rpm
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
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 |
20 |
21 |
22 |
23 | {% if let Some(license) = package.license %}
24 | {{ license }}
25 | {% endif %}
26 | {% if let Some(vendor) = package.vendor %}
27 | {{ vendor }}
28 | {% endif %}
29 | {% if let Some(group) = package.group %}
30 | {{ group }}
31 | {% endif %}
32 | {% if let Some(build_host) = package.build_host %}
33 | {{ build_host }}
34 | {% endif %}
35 | {% if let Some(source) = package.source %}
36 | {{ source }}
37 | {% endif %}
38 |
39 |
40 | {% for provide in package.provides %}
41 | {% if provide.version == "" %}
42 |
43 | {% else %}
44 |
45 | {% endif %}
46 | {% endfor %}
47 |
48 |
49 | {% for require in package.requires %}
50 | {% if require.version == "" %}
51 |
52 | {% else %}
53 |
54 | {% endif %}
55 | {% endfor %}
56 |
57 |
58 |
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 |
--------------------------------------------------------------------------------