This commit is contained in:
JMARyA 2024-12-26 00:37:50 +01:00
commit 221b2a82e7
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
7 changed files with 3817 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

3534
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "pacco"
version = "0.1.0"
edition = "2024"
[dependencies]
based = { git = "https://git.hydrar.de/jmarya/based", features = ["htmx"]}
env_logger = "0.11.6"
maud = "0.26.0"
rocket = "0.5.1"
sqlx = "0.8.2"

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# Pacco
Pacco is an application for managing and hosting pacman repositories.
## Features
- Multiple repositories
- Multiple architectures
- Web UI for packages
- API for pushing new packages
- Smart mirroring

36
src/main.rs Normal file
View file

@ -0,0 +1,36 @@
// TODO :
// - Base
// - API
// - UI
// - PkgDB Abstraction
// - Pkg Abstraction
use based::page::{Shell, render_page};
use based::request::{RequestContext, StringResponse};
use maud::html;
use rocket::get;
use rocket::routes;
pub mod pkg;
pub mod routes;
#[get("/")]
pub async fn index_page(ctx: RequestContext) -> StringResponse {
let content = html!(
h1 { "Hello World!" };
);
render_page(
content,
"Hello World",
ctx,
&Shell::new(html! {}, html! {}, Some(String::new())),
)
.await
}
#[rocket::launch]
async fn launch() -> _ {
env_logger::init();
rocket::build().mount("/", routes![index_page, routes::pkg_route])
}

154
src/pkg.rs Normal file
View file

@ -0,0 +1,154 @@
use std::path::{Path, PathBuf};
pub struct Repository {
pub name: String,
}
impl Repository {
pub fn new(name: &str) -> Option<Self> {
if PathBuf::from("./data").join(name).exists() {
Some(Repository {
name: name.to_string(),
})
} else {
None
}
}
pub fn base_path(&self, arch: &str) -> PathBuf {
PathBuf::from("./data").join(&self.name).join(arch)
}
pub fn db_content(&self, arch: &str) -> Option<Vec<u8>> {
std::fs::read(
self.base_path(arch)
.join(format!("{}.db.tar.gz", self.name)),
)
.ok()
}
pub fn sig_content(&self, arch: &str) -> Option<Vec<u8>> {
std::fs::read(
self.base_path(arch)
.join(format!("{}.db.tar.gz.sig", self.name)),
)
.ok()
}
pub fn get_pkg(&self, arch: &str, pkg_name: &str) -> Option<Package> {
let pkg = if pkg_name.ends_with(".pkg.tar.zst") {
Package::new(&self.name, arch, pkg_name.trim_end_matches(".pkg.tar.zst"))
} else if pkg_name.ends_with(".pkg.tar.zst.sig") {
Package::new(
&self.name,
arch,
pkg_name.trim_end_matches(".pkg.tar.zst.sig"),
)
} else {
Package::new(&self.name, arch, pkg_name)
};
if pkg.exists() {
return Some(pkg);
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct Package {
repo: String,
arch: String,
name: String,
version: Option<String>,
}
impl Package {
pub fn new(repo: &str, arch: &str, pkg_name: &str) -> Self {
Package {
repo: repo.to_string(),
arch: arch.to_string(),
name: pkg_name.to_string(),
version: None,
}
}
fn base_path(&self) -> PathBuf {
Path::new("./data").join(&self.repo).join(&self.arch)
}
pub fn switch_arch(&self, arch: &str) -> Self {
let mut new = self.clone();
new.arch = arch.to_string();
new
}
pub fn exists(&self) -> bool {
let pkg_file = self.base_path().join(self.file_name());
pkg_file.exists()
}
pub fn get_version(&self, version: &str) -> Self {
let mut new_pkg = self.clone();
new_pkg.version = Some(version.to_string());
new_pkg
}
pub fn is_signed(&self) -> bool {
let signed_file = self.base_path().join(format!("{}.sig", self.file_name()));
signed_file.exists()
}
pub fn file_name(&self) -> String {
format!(
"{}-{}-{}-{}.pkg.tar.zst",
self.name,
if let Some(ver) = &self.version {
ver.to_string()
} else {
let versions = self.versions();
versions.first().unwrap().clone()
},
1,
self.arch,
)
}
pub fn versions(&self) -> Vec<String> {
let dir_path = self.base_path();
let mut versions = vec![];
if let Ok(entries) = std::fs::read_dir(dir_path) {
for entry in entries.filter_map(Result::ok) {
let file_name = entry.file_name().into_string().unwrap_or_default();
if file_name.starts_with(&self.name) && file_name.ends_with(".pkg.tar.zst") {
// Extract version (assuming the filename is "<pkg_name>-<version>-<relation>-<arch>.pkg.tar.zst")
if let Some(version) = file_name.split('-').nth(1) {
versions.push(version.to_string());
}
}
}
}
// Sort versions in descending order (most recent version first)
versions.sort_by(|a, b| b.cmp(a));
versions
}
pub fn pkg_content(&self) -> Option<Vec<u8>> {
if self.exists() {
return std::fs::read(self.base_path().join(self.file_name())).ok();
}
None
}
pub fn sig_content(&self) -> Option<Vec<u8>> {
if self.exists() {
return std::fs::read(self.base_path().join(format!("{}.sig", &self.file_name()))).ok();
}
None
}
}

72
src/routes/mod.rs Normal file
View file

@ -0,0 +1,72 @@
use based::page::{Shell, render_page};
use based::request::{RawResponse, RequestContext, StringResponse, respond_with};
use maud::html;
use rocket::get;
use rocket::http::{ContentType, Status};
use crate::pkg::Repository;
// /pkg/<repo>/<arch>/<pkg_name>
// /pkg/<repo>/<arch>/
#[get("/pkg/<repo>/<arch>/<pkg_name>")]
pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str, ctx: RequestContext) -> RawResponse {
if let Some(repo) = Repository::new(repo) {
if 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")
{
if pkg_name.ends_with("sig") {
return respond_with(
Status::Ok,
ContentType::new("application", "pgp-signature"),
repo.sig_content(arch).unwrap(),
);
} else {
return respond_with(
Status::Ok,
ContentType::new("application", "tar"),
repo.db_content(arch).unwrap(),
);
}
}
let pkg = repo.get_pkg(arch, pkg_name).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(),
);
}
}
respond_with(
Status::Ok,
ContentType::Plain,
"Not found".as_bytes().to_vec(),
)
}
#[get("/")]
pub async fn index_page(ctx: RequestContext) -> StringResponse {
let content = html!(
h1 { "Hello World!" };
);
render_page(
content,
"Hello World",
ctx,
&Shell::new(html! {}, html! {}, Some(String::new())),
)
.await
}