smart mirroring

This commit is contained in:
JMARyA 2025-01-13 11:39:00 +01:00
parent 68cb32f07b
commit 7647616242
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
13 changed files with 701 additions and 67 deletions

348
Cargo.lock generated
View file

@ -153,6 +153,12 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -209,8 +215,8 @@ dependencies = [
"rand", "rand",
"rayon", "rayon",
"regex", "regex",
"reqwest", "reqwest 0.11.27",
"ring", "ring 0.16.20",
"rocket", "rocket",
"rocket_cors", "rocket_cors",
"serde", "serde",
@ -900,6 +906,25 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "h2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http 1.2.0",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@ -1010,6 +1035,29 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.2.0",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"pin-project-lite",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.9.5" version = "1.9.5"
@ -1038,9 +1086,9 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.26",
"http 0.2.12", "http 0.2.12",
"http-body", "http-body 0.4.6",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -1052,6 +1100,43 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.7",
"http 1.2.0",
"http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.2.0",
"hyper 1.5.2",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.5.0" version = "0.5.0"
@ -1059,12 +1144,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [ dependencies = [
"bytes", "bytes",
"hyper", "hyper 0.14.32",
"native-tls", "native-tls",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
] ]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.5.2",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.5.2",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.61" version = "0.1.61"
@ -1643,10 +1763,13 @@ dependencies = [
"env_logger 0.11.6", "env_logger 0.11.6",
"log", "log",
"maud", "maud",
"reqwest 0.12.12",
"rocket", "rocket",
"serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"tar", "tar",
"toml",
"zstd", "zstd",
] ]
@ -1959,11 +2082,11 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.26",
"http 0.2.12", "http 0.2.12",
"http-body", "http-body 0.4.6",
"hyper", "hyper 0.14.32",
"hyper-tls", "hyper-tls 0.5.0",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
@ -1972,12 +2095,12 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls-pemfile", "rustls-pemfile 1.0.4",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper 0.1.2",
"system-configuration", "system-configuration 0.5.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tower-service", "tower-service",
@ -1988,6 +2111,50 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "reqwest"
version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.4.7",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper-rustls",
"hyper-tls 0.6.0",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tower",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.16.20" version = "0.16.20"
@ -1998,11 +2165,26 @@ dependencies = [
"libc", "libc",
"once_cell", "once_cell",
"spin 0.5.2", "spin 0.5.2",
"untrusted", "untrusted 0.7.1",
"web-sys", "web-sys",
"winapi", "winapi",
] ]
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rocket" name = "rocket"
version = "0.5.1" version = "0.5.1"
@ -2085,7 +2267,7 @@ dependencies = [
"either", "either",
"futures", "futures",
"http 0.2.12", "http 0.2.12",
"hyper", "hyper 0.14.32",
"indexmap", "indexmap",
"log", "log",
"memchr", "memchr",
@ -2141,6 +2323,19 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "rustls"
version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.4" version = "1.0.4"
@ -2150,6 +2345,32 @@ dependencies = [
"base64 0.21.7", "base64 0.21.7",
] ]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring 0.17.8",
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.19" version = "1.0.19"
@ -2631,6 +2852,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.13.1" version = "0.13.1"
@ -2650,7 +2880,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.7.0",
"core-foundation",
"system-configuration-sys 0.6.0",
] ]
[[package]] [[package]]
@ -2663,6 +2904,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "tar" name = "tar"
version = "0.4.43" version = "0.4.43"
@ -2822,6 +3073,16 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.17" version = "0.1.17"
@ -2880,6 +3141,27 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -3034,6 +3316,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -3258,6 +3546,36 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -10,8 +10,11 @@ chrono = "0.4.39"
env_logger = "0.11.6" env_logger = "0.11.6"
log = "0.4.22" log = "0.4.22"
maud = "0.26.0" maud = "0.26.0"
reqwest = { version = "0.12.12" }
rocket = "0.5.1" rocket = "0.5.1"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134" serde_json = "1.0.134"
sqlx = "0.8.2" sqlx = "0.8.2"
tar = "0.4.43" tar = "0.4.43"
toml = "0.8.19"
zstd = "0.13.2" zstd = "0.13.2"

15
config.toml Normal file
View file

@ -0,0 +1,15 @@
[mirror]
repos = [
"core",
"extra",
"multilib"
]
mirrorlist = [
"http://mirrors.kernel.org/archlinux/$repo/os/$arch",
"https://geo.mirror.pkgbuild.com/$repo/os/$arch",
"https://ftpmirror.infania.net/mirror/archlinux/$repo/os/$arch",
"http://mirror.rackspace.com/archlinux/$repo/os/$arch",
"https://mirror.rackspace.com/archlinux/$repo/os/$arch"
]

View file

@ -5,6 +5,7 @@ services:
- "8080:8000" - "8080:8000"
volumes: volumes:
- ./data:/data - ./data:/data
- ./config.toml:/config.toml
environment: environment:
- "RUST_LOG=info" - "RUST_LOG=info"
- "ROCKET_ADDRESS=0.0.0.0" - "ROCKET_ADDRESS=0.0.0.0"

30
src/config.rs Normal file
View file

@ -0,0 +1,30 @@
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Config {
pub mirror: Option<MirrorConfig>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct MirrorConfig {
repos: Vec<String>,
mirrorlist: Vec<String>,
}
impl Config {
pub fn is_mirrored_repo(&self, repo: &str) -> bool {
if let Some(mirrorc) = &self.mirror {
return mirrorc.repos.iter().any(|x| x == repo);
}
false
}
pub fn mirrorlist(&self) -> Option<&[String]> {
if let Some(mirror) = &self.mirror {
return Some(mirror.mirrorlist.as_slice());
}
None
}
}

View file

@ -1,7 +1,9 @@
use based::auth::User; use based::auth::User;
use based::get_pg; use based::get_pg;
use config::Config;
use rocket::routes; use rocket::routes;
pub mod config;
pub mod routes; pub mod routes;
#[rocket::launch] #[rocket::launch]
@ -11,9 +13,14 @@ async fn launch() -> _ {
let pg = get_pg!(); let pg = get_pg!();
sqlx::migrate!("./migrations").run(pg).await.unwrap(); sqlx::migrate!("./migrations").run(pg).await.unwrap();
let config: Config =
toml::from_str(&std::fs::read_to_string("config.toml").unwrap_or_default())
.unwrap_or_default();
let _ = User::create("admin".to_string(), "admin", based::auth::UserRole::Admin).await; let _ = User::create("admin".to_string(), "admin", based::auth::UserRole::Admin).await;
rocket::build().mount("/", routes![ rocket::build()
.mount("/", routes![
based::htmx::htmx_script_route, based::htmx::htmx_script_route,
routes::index_page, routes::index_page,
routes::pkg_route, routes::pkg_route,
@ -28,4 +35,5 @@ async fn launch() -> _ {
routes::user::change_password, routes::user::change_password,
routes::user::change_password_post routes::user::change_password_post
]) ])
.manage(config)
} }

120
src/pkg/mirror.rs Normal file
View file

@ -0,0 +1,120 @@
use std::path::PathBuf;
use super::{Package, Repository, arch::Architecture};
pub struct MirrorRepository {
pub inner: Repository,
}
impl MirrorRepository {
pub fn new(repo: &str) -> Self {
std::fs::create_dir_all(format!("./data/{repo}")).unwrap();
Self {
inner: Repository::new(repo).unwrap(),
}
}
pub async fn download_file(
&self,
url: &str,
file_path: PathBuf,
mirrorlist: &[String],
arch: Architecture,
) {
log::info!("Downloading {url} to {}", file_path.to_str().unwrap());
for mirror in mirrorlist {
let mirror = mirror
.replace("$repo", &self.inner.name)
.replace("$arch", &arch.to_string());
let url = format!("{mirror}/{url}");
log::info!("Trying mirror {url}");
let client = reqwest::Client::new();
if let Ok(resp) = client.get(url).send().await {
if resp.status().is_success() {
let data = resp.bytes().await.unwrap().to_vec();
let parent = file_path.parent().unwrap();
std::fs::create_dir_all(parent).unwrap();
std::fs::write(file_path, data).unwrap();
return;
} else {
log::warn!("Mirror {mirror} failed [{}]", resp.status().as_u16());
}
}
}
}
/// Get the `.db.tar.gz` content for the repository of `arch`
pub async fn db_content(&self, arch: Architecture, mirrorlist: &[String]) -> Option<Vec<u8>> {
if let Some(content) = self.inner.db_content(arch.clone()) {
return Some(content);
}
self.download_file(
&format!("{}.db.tar.gz", self.inner.name),
self.inner
.base_path(arch.clone())
.join(format!("{}.db.tar.gz", self.inner.name)),
mirrorlist,
arch.clone(),
)
.await;
self.inner.db_content(arch)
}
/// Get the `.db.tar.gz.sig` content for the repository of `arch`
pub async fn sig_content(&self, arch: Architecture, mirrorlist: &[String]) -> Option<Vec<u8>> {
if let Some(content) = self.inner.sig_content(arch.clone()) {
return Some(content);
}
self.download_file(
&format!("{}.db.tar.gz.sig", self.inner.name),
self.inner
.base_path(arch.clone())
.join(format!("{}.db.tar.gz.sig", self.inner.name)),
mirrorlist,
arch.clone(),
)
.await;
self.inner.sig_content(arch)
}
pub async fn get_pkg(&self, pkg_name: &str, mirrorlist: &[String]) -> Option<Package> {
if let Some(pkg) = self.inner.get_pkg(pkg_name) {
return Some(pkg);
}
// PKG
let (name, _, _, arch) = Package::extract_pkg_name(pkg_name).unwrap();
self.download_file(
pkg_name,
self.inner
.base_path(arch.clone())
.join(&name)
.join(pkg_name),
mirrorlist,
arch.clone(),
)
.await;
// SIG
self.download_file(
&format!("{pkg_name}.sig"),
self.inner
.base_path(arch.clone())
.join(&name)
.join(format!("{pkg_name}.sig")),
mirrorlist,
arch.clone(),
)
.await;
self.inner.get_pkg(pkg_name)
}
}

View file

@ -1,8 +1,20 @@
// TODO : Read DB Info
pub mod repo; pub mod repo;
pub use repo::Repository; pub use repo::Repository;
pub mod package; pub mod package;
pub use package::Package; pub use package::Package;
pub mod arch; pub mod arch;
pub mod db; pub mod db;
pub mod mirror;
pub fn find_package_by_name(pkg: &str) -> Option<Package> {
for repo in Repository::list() {
let repo = Repository::new(&repo).unwrap();
let pkgs = repo.list_pkg();
if let Some(res) = pkgs.iter().find(|x| *x == pkg) {
return repo.get_pkg_by_name(res);
}
}
None
}

View file

@ -18,18 +18,21 @@ pub struct Package {
/// Name of the package /// Name of the package
pub name: String, pub name: String,
/// Version of the package /// Version of the package
version: Option<String>, pub version: Option<String>,
} }
impl Package { impl Package {
/// Create a new package /// Create a new package
pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str) -> Self { pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str) -> Self {
Package { let pkg = Package {
repo: repo.to_string(), repo: repo.to_string(),
arch, arch,
name: pkg_name.to_string(), name: pkg_name.to_string(),
version: Some(version.to_string()), version: Some(version.to_string()),
} };
std::fs::create_dir_all(pkg.base_path()).unwrap();
pkg
} }
pub fn install_script(&self) -> Option<String> { pub fn install_script(&self) -> Option<String> {
@ -186,7 +189,7 @@ impl Package {
pub fn pacman_hooks(&self) -> Vec<(String, String)> { pub fn pacman_hooks(&self) -> Vec<(String, String)> {
let pkg_file = self.base_path().join(self.file_name()); let pkg_file = self.base_path().join(self.file_name());
let files = list_tar_file(&pkg_file).unwrap_or_default(); let files = self.file_list();
files files
.into_iter() .into_iter()
.filter(|x| { .filter(|x| {
@ -237,12 +240,10 @@ impl Package {
fn base_path(&self) -> PathBuf { fn base_path(&self) -> PathBuf {
// <repo>/<arch>/<pkg>/ // <repo>/<arch>/<pkg>/
let p = Path::new("./data") Path::new("./data")
.join(&self.repo) .join(&self.repo)
.join(self.arch.to_string()) .join(self.arch.to_string())
.join(&self.name); .join(&self.name)
std::fs::create_dir_all(&p).unwrap();
p
} }
/// Switch the `Architecture` of the package /// Switch the `Architecture` of the package

View file

@ -18,6 +18,7 @@ impl Repository {
repos.push(file_name); repos.push(file_name);
} }
repos.sort();
repos repos
} }

View file

@ -2,18 +2,25 @@ use based::auth::MaybeUser;
use based::page::{Shell, htmx_link, render_page}; use based::page::{Shell, htmx_link, render_page};
use based::request::{RawResponse, RequestContext, StringResponse, respond_with}; use based::request::{RawResponse, RequestContext, StringResponse, respond_with};
use maud::{PreEscaped, html}; use maud::{PreEscaped, html};
use rocket::get; use pacco::pkg::mirror::MirrorRepository;
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::{State, get};
use pacco::pkg::Repository; use pacco::pkg::Repository;
use pacco::pkg::arch::Architecture; use pacco::pkg::arch::Architecture;
use crate::config::Config;
pub mod push; pub mod push;
pub mod ui; pub mod ui;
pub mod user; pub mod user;
#[get("/")] #[get("/")]
pub async fn index_page(ctx: RequestContext, user: MaybeUser) -> StringResponse { pub async fn index_page(
ctx: RequestContext,
user: MaybeUser,
config: &State<Config>,
) -> StringResponse {
let repos: Vec<String> = Repository::list(); let repos: Vec<String> = Repository::list();
let content = html!( let content = html!(
@ -26,8 +33,12 @@ pub async fn index_page(ctx: RequestContext, user: MaybeUser) -> StringResponse
div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" { div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" {
@for repo in repos { @for repo in repos {
(htmx_link(&format!("/{repo}"), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! { (htmx_link(&format!("/{repo}"), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! {
p class="font-medium flex-1 text-gray-800 dark:text-gray-100" { p class="font-medium text-gray-800 dark:text-gray-100" {
(repo) (repo)
@if config.is_mirrored_repo(&repo) {
div class="inline-block px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-full" { "Mirrored" };
};
}; };
})) }))
} }
@ -38,15 +49,64 @@ pub async fn index_page(ctx: RequestContext, user: MaybeUser) -> StringResponse
} }
#[get("/pkg/<repo>/<arch>/<pkg_name>")] #[get("/pkg/<repo>/<arch>/<pkg_name>")]
pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str) -> RawResponse { pub async fn pkg_route(
repo: &str,
arch: &str,
pkg_name: &str,
config: &State<Config>,
) -> RawResponse {
let arch = Architecture::parse(arch).unwrap(); let arch = Architecture::parse(arch).unwrap();
if config.is_mirrored_repo(repo) {
let repo = MirrorRepository::new(repo);
if is_repo_db(pkg_name) {
if pkg_name.ends_with("sig") {
return respond_with(
Status::Ok,
ContentType::new("application", "pgp-signature"),
repo.sig_content(arch, config.mirrorlist().unwrap())
.await
.unwrap(),
);
} else {
return respond_with(
Status::Ok,
ContentType::new("application", "tar"),
repo.db_content(arch, config.mirrorlist().unwrap())
.await
.unwrap(),
);
}
}
let pkg = repo
.get_pkg(pkg_name, config.mirrorlist().unwrap())
.await
.unwrap();
if pkg_name.ends_with("pkg.tar.zst") {
return respond_with(
Status::Ok,
ContentType::new("application", "tar"),
pkg.pkg_content().unwrap(),
);
} else if pkg_name.ends_with("pkg.tar.zst.sig") {
return respond_with(
Status::Ok,
ContentType::new("application", "pgp-signature"),
pkg.sig_content().unwrap(),
);
}
return respond_with(
Status::Ok,
ContentType::Plain,
"Not found".as_bytes().to_vec(),
);
}
if let Some(repo) = Repository::new(repo) { if let Some(repo) = Repository::new(repo) {
if pkg_name.ends_with("db.tar.gz") if is_repo_db(pkg_name) {
|| pkg_name.ends_with("db")
|| pkg_name.ends_with("db.tar.gz.sig")
|| pkg_name.ends_with("db.sig")
{
if pkg_name.ends_with("sig") { if pkg_name.ends_with("sig") {
return respond_with( return respond_with(
Status::Ok, Status::Ok,
@ -107,3 +167,10 @@ pub async fn render(
) )
.await .await
} }
pub fn is_repo_db(pkg_name: &str) -> bool {
pkg_name.ends_with("db.tar.gz")
|| pkg_name.ends_with("db")
|| pkg_name.ends_with("db.tar.gz.sig")
|| pkg_name.ends_with("db.sig")
}

View file

@ -1,6 +1,6 @@
use based::request::api::{FallibleApiResponse, api_error}; use based::request::api::{FallibleApiResponse, api_error};
use rocket::tokio::io::AsyncReadExt; use rocket::tokio::io::AsyncReadExt;
use rocket::{FromForm, post}; use rocket::{FromForm, State, post};
use serde_json::json; use serde_json::json;
use pacco::pkg::Package; use pacco::pkg::Package;
@ -12,6 +12,8 @@ use pacco::pkg::arch::Architecture;
use rocket::form::Form; use rocket::form::Form;
use rocket::fs::TempFile; use rocket::fs::TempFile;
use crate::config::Config;
#[derive(FromForm)] #[derive(FromForm)]
pub struct PkgUpload<'r> { pub struct PkgUpload<'r> {
name: String, name: String,
@ -37,12 +39,17 @@ pub async fn upload_pkg(
repo: &str, repo: &str,
upload: Form<PkgUpload<'_>>, upload: Form<PkgUpload<'_>>,
user: based::auth::APIUser, user: based::auth::APIUser,
config: &State<Config>,
) -> FallibleApiResponse { ) -> FallibleApiResponse {
// TODO : Permission System // TODO : Permission System
if !user.0.is_admin() { if !user.0.is_admin() {
return Err(api_error("Forbidden")); return Err(api_error("Forbidden"));
} }
if config.is_mirrored_repo(repo) {
return Err(api_error("This repository is a mirror."));
}
let pkg = Package::new( let pkg = Package::new(
repo, repo,
Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?, Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?,

View file

@ -3,18 +3,30 @@ use based::{
request::{RequestContext, StringResponse}, request::{RequestContext, StringResponse},
}; };
use maud::{PreEscaped, html}; use maud::{PreEscaped, html};
use rocket::get; use rocket::{State, get};
use pacco::pkg::{Repository, arch::Architecture}; use pacco::pkg::{Repository, arch::Architecture, find_package_by_name};
use crate::config::Config;
use super::render; use super::render;
// TODO : API // TODO : API
#[get("/<repo>/<pkg_name>")] #[get("/<repo>/<pkg_name>?<ver>")]
pub async fn pkg_ui(repo: &str, pkg_name: &str, ctx: RequestContext) -> Option<StringResponse> { pub async fn pkg_ui(
repo: &str,
pkg_name: &str,
ctx: RequestContext,
ver: Option<&str>,
) -> Option<StringResponse> {
let repo = Repository::new(repo).unwrap(); let repo = Repository::new(repo).unwrap();
let pkg = repo.get_pkg_by_name(pkg_name)?; let mut pkg = repo.get_pkg_by_name(pkg_name)?;
if let Some(ver) = ver {
pkg = pkg.get_version(ver);
}
let versions = pkg.versions(); let versions = pkg.versions();
let arch = pkg.arch(); let arch = pkg.arch();
let install_script = pkg.install_script(); let install_script = pkg.install_script();
@ -88,9 +100,10 @@ pub async fn pkg_ui(repo: &str, pkg_name: &str, ctx: RequestContext) -> Option<S
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { "Versions" } h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { "Versions" }
ul class="space-y-1" { ul class="space-y-1" {
@for version in versions { @for version in versions {
// TODO : Implement page per version ?version=
li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" { li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" {
(htmx_link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name), if pkg.version.as_ref().map(|x| *x == version).unwrap_or_default() { "text-blue-500" } else { "" }, "", html! {
(version) (version)
}))
}; };
} }
} }
@ -166,8 +179,12 @@ pub async fn pkg_ui(repo: &str, pkg_name: &str, ctx: RequestContext) -> Option<S
} }
#[get("/<repo>?<arch>")] #[get("/<repo>?<arch>")]
pub async fn repo_ui(repo: &str, ctx: RequestContext, arch: Option<&str>) -> StringResponse { pub async fn repo_ui(
// TODO : permissions repo: &str,
ctx: RequestContext,
arch: Option<&str>,
config: &State<Config>,
) -> StringResponse {
let arch = arch.map(|x| Architecture::parse(x).unwrap_or(Architecture::any)); let arch = arch.map(|x| Architecture::parse(x).unwrap_or(Architecture::any));
let repo = Repository::new(repo).unwrap(); let repo = Repository::new(repo).unwrap();
@ -181,13 +198,16 @@ pub async fn repo_ui(repo: &str, ctx: RequestContext, arch: Option<&str>) -> Str
let content = html! { let content = html! {
// Repository name and architectures // Repository name and architectures
div class="flex flex-wrap items-center justify-center mb-6" { div class="flex flex-wrap items-center justify-center mb-6" {
h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-4" { h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100" {
(repo.name) (repo.name)
}; };
div class="flex gap-2 mt-2 md:mt-0" { @if config.is_mirrored_repo(&repo.name) {
div class="inline-block px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-full ml-4" { "Mirrored" };
};
div class="flex gap-2 mt-2 md:mt-0 ml-4" {
@for a in architectures { @for a in architectures {
// TODO : Filter per arch with ?arch=
@if let Some(arch) = arch.as_ref() { @if let Some(arch) = arch.as_ref() {
@if arch.to_string() == a { @if arch.to_string() == a {
(htmx_link(&format!("/{}", repo.name), "px-3 py-1 text-sm font-medium bg-blue-400 dark:bg-blue-500 text-gray-600 dark:text-gray-300 rounded-full", "", html! { (htmx_link(&format!("/{}", repo.name), "px-3 py-1 text-sm font-medium bg-blue-400 dark:bg-blue-500 text-gray-600 dark:text-gray-300 rounded-full", "", html! {
@ -258,12 +278,10 @@ pub fn build_info(key: String, value: String) -> PreEscaped<String> {
); );
} }
"depend" => { "depend" => {
// TODO : Find package link return pkg_list_info("Depends on", &value);
return key_value("Depends on".to_string(), value);
} }
"makedepend" => { "makedepend" => {
// TODO : Find package link return pkg_list_info("Build Dependencies", &value);
return key_value("Build Dependencies".to_string(), value);
} }
"license" => { "license" => {
return key_value("License".to_string(), value); return key_value("License".to_string(), value);
@ -307,3 +325,36 @@ pub fn take_out<T>(v: &mut Vec<T>, f: impl Fn(&T) -> bool) -> (&mut Vec<T>, Opti
(v, None) (v, None)
} }
pub fn find_pkg_url(pkg: &str) -> Option<String> {
if let Some(pkg) = find_package_by_name(pkg) {
return Some(format!("/{}/{}", pkg.repo, pkg.name));
}
None
}
pub fn pkg_list_info(key: &str, value: &str) -> PreEscaped<String> {
let pkgs = value.split_whitespace().map(|pkg| {
if let Some(pkg_url) = find_pkg_url(pkg) {
html! {
a href=(pkg_url) class="ml-2 text-blue-400" { (pkg) };
}
} else {
html! {
span class="ml-2" { (pkg) };
}
}
});
html! {
div class="flex items-center" {
span class="font-bold w-32" { (format!("{key}: ")) };
div class="flex flex-wrap" {
@for pkg in pkgs {
(pkg)
};
};
};
}
}