🔀 Merge Download Count Feature

This commit is contained in:
JMARyA 2025-01-26 11:16:43 +01:00
commit 249f8d873c
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
5 changed files with 635 additions and 3 deletions

View file

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS package_meta (
repo TEXT NOT NULL,
name TEXT NOT NULL,
arch TEXT NOT NULL,
version TEXT NOT NULL,
rel INTEGER NOT NULL,
download_count INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (repo, name, arch, version, rel)
);

View file

@ -4,6 +4,8 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use based::get_pg;
use sqlx::FromRow;
use tar::Archive; use tar::Archive;
use super::{Repository, arch::Architecture}; use super::{Repository, arch::Architecture};
@ -17,14 +19,14 @@ pub struct Package {
pub arch: Architecture, pub arch: Architecture,
/// Name of the package /// Name of the package
pub name: String, pub name: String,
pub rel: u64, pub rel: i32,
/// Version of the package /// Version of the package
pub 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, rel: u64) -> Self { pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str, rel: i32) -> Self {
let pkg = Package { let pkg = Package {
repo: repo.to_string(), repo: repo.to_string(),
arch, arch,
@ -37,7 +39,7 @@ impl Package {
pkg pkg
} }
pub fn version(ver: &str) -> (String, u64) { pub fn version(ver: &str) -> (String, i32) {
let mut splitted = ver.split('-').collect::<Vec<_>>(); let mut splitted = ver.split('-').collect::<Vec<_>>();
let rel = splitted.pop().unwrap(); let rel = splitted.pop().unwrap();
let ver = splitted.join("-"); let ver = splitted.join("-");
@ -504,3 +506,42 @@ pub fn list_tar_file(tar: &PathBuf) -> Option<Vec<String>> {
Some(paths) Some(paths)
} }
#[derive(Debug, Clone, FromRow)]
pub struct PackageMeta {
pub repo: String,
pub name: String,
pub arch: String,
pub version: String,
pub rel: String,
pub download_count: i32,
}
pub trait PackageMetaInfo {
fn download_amount(&self) -> impl std::future::Future<Output = i32>;
fn increase_download_count(&self) -> impl std::future::Future<Output = ()>;
}
impl PackageMetaInfo for Package {
async fn download_amount(&self) -> i32 {
let res: Option<(i32,)> = sqlx::query_as("SELECT download_count FROM package_meta WHERE repo = $1 AND name = $2 AND version = $3 AND arch = $4 AND rel = $5")
.bind(&self.repo)
.bind(&self.name)
.bind(self.version.as_ref().unwrap())
.bind(&self.arch.to_string())
.bind(&self.rel)
.fetch_optional(get_pg!()).await.unwrap();
res.map(|x| x.0).unwrap_or(0)
}
async fn increase_download_count(&self) {
sqlx::query("INSERT INTO package_meta (repo, name, arch, version, rel) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (repo, name, arch, version, rel) DO UPDATE SET download_count = package_meta.download_count + 1")
.bind(&self.repo)
.bind(&self.name)
.bind(&self.arch.to_string())
.bind(&self.version.as_ref().map(|x| x.to_string()).unwrap_or_default())
.bind(&self.rel)
.execute(get_pg!()).await.unwrap();
}
}

View file

@ -4,6 +4,7 @@ use based::ui::components::Shell;
use based::ui::prelude::*; use based::ui::prelude::*;
use maud::{Render, html}; use maud::{Render, html};
use pacco::pkg::mirror::MirrorRepository; use pacco::pkg::mirror::MirrorRepository;
use pacco::pkg::package::PackageMetaInfo;
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::{State, get}; use rocket::{State, get};
@ -99,6 +100,7 @@ pub async fn pkg_route(
.unwrap(); .unwrap();
if pkg_name.ends_with("pkg.tar.zst") { if pkg_name.ends_with("pkg.tar.zst") {
pkg.increase_download_count().await;
return respond_with( return respond_with(
Status::Ok, Status::Ok,
ContentType::new("application", "tar"), ContentType::new("application", "tar"),

571
src/routes/ui.rs Normal file
View file

@ -0,0 +1,571 @@
use based::request::{RequestContext, StringResponse};
use based::ui::primitives::flex::Strategy;
use based::ui::primitives::space::SpaceBetweenWidget;
use based::ui::primitives::text::{Code, TextWidget};
use based::ui::{UIWidget, prelude::*};
use maud::{PreEscaped, Render, html};
use pacco::pkg::package::PackageMetaInfo;
use rocket::{State, get};
use pacco::pkg::{Package, Repository, arch::Architecture, find_package_by_name};
use crate::config::Config;
use super::render;
// TODO : API
#[get("/<repo>/<pkg_name>?<ver>")]
pub async fn pkg_ui(
repo: &str,
pkg_name: &str,
ctx: RequestContext,
ver: Option<&str>,
) -> Option<StringResponse> {
let repo = Repository::new(repo).unwrap();
let mut pkg = repo.get_pkg_by_name(pkg_name)?;
if let Some(ver) = ver {
let (version, rel) = Package::version(ver);
pkg = pkg.get_version(&version);
pkg.rel = rel;
}
let dl_count = pkg.download_amount().await;
let versions = pkg.versions();
let arch = pkg.arch();
let install_script = pkg.install_script();
let systemd_units = pkg.systemd_units();
let pacman_hooks = pkg.pacman_hooks();
let binaries = pkg.binaries();
let mut pkginfo = pkg.pkginfo();
let content = Div().vanish()
.push(
Flex(
// Package Name
Div().vanish()
.push(
Link(&format!("/{}", repo.name),
Text(&repo.name).bold().color(&Gray::_100)._3xl()
).use_htmx()
)
.push(Padding(Text("/").bold().color(&Gray::_400)).all(ScreenValue::_2))
.push(
Padding(Text(&pkg.name)._3xl().bold().color(&Gray::_100)).right(ScreenValue::_2)
)
.push_if(pkg.is_signed(), || {
Flex(
Padding(
Div().vanish()
.push(Span("")._2xl().bold().color(&Slate::_300))
.push(Span("Signed").sm().medium().color(&Slate::_300))
// TODO : Add more info: Who signed? + Public Key recv
).right(ScreenValue::_4)
).items_center().gap(ScreenValue::_2)
})
.push_for_each(&arch, |arch: &Architecture| {
Rounded(
Padding(
Background(Gray::_700,
Span(&arch.to_string()).medium().sm())
).x(ScreenValue::_3).y(ScreenValue::_1)
).size(Size::Full)
})
.push(Margin(
Flex(
Padding(
Hover(Background(Gray::_300, Nothing())).on(Background(Gray::_200,
Rounded(Shadow::medium(
Link(&format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name()),
Paragraph(
html! {
svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" {
path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m7-7l-7 7-7-7" {};
};
p class="ml-2 text-black" { "Download" };
}
).black().xs()))).size(Size::Large)
))
).x(ScreenValue::_2).y(ScreenValue::_2)
).items_center()
).left(ScreenValue::_4)
)
).wrap(Wrap::Wrap).gap(ScreenValue::_2).full_center().group()
)
.push(
Padding(
Flex(
Div().vanish().push(
Context(
FlexGrow(Strategy::Grow,
Animated(Background(Gray::_800, Shadow::large(Rounded(Margin(Padding(SpaceBetween(
Div()
.push(
Context(Text("Info").xl().semibold().color(&Gray::_300).underlined())
)
.push_some(
take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push_some(
take_out(&mut pkginfo, |x| { x.0 == "packager" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push_some(
take_out(&mut pkginfo, |x| { x.0 == "url" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push_some(
take_out(&mut pkginfo, |x| { x.0 == "size" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push(
build_info("download_amount".to_string(), dl_count.to_string())
)
.push_for_each(&pkginfo, |(key, val)| build_info(key.clone(), val.clone()))
).y(ScreenValue::_2)).all(ScreenValue::_4)).all(ScreenValue::_2)).size(Size::Large)))))
)).push(
SpaceBetween(
Padding(
Background(Gray::_800,
Shadow::large(
Rounded(
Margin(
Screen::medium(FlexGrow(Strategy::NoGrow, Nothing())).on(
FlexGrow(Strategy::Grow,
Div().vanish()
.push(
Text("Versions").xl().semibold().color(&Gray::_300)
)
.push(
SpaceBetween(
Div().vanish()
.push_for_each(&versions, |version: &String| {
Animated(Link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name),
if pkg.version.as_ref()
.map(|x| *x == Package::version(&version).0).unwrap_or_default()
{ Text(&version).color(&Blue::_500).render() } else {
Hover(Text("").color(&Blue::_400)).on(
Text(&version).color(&Gray::_100)).render()
}
).use_htmx())
})
).y(ScreenValue::_1)
)
))
).all(ScreenValue::_2)
).size(Size::Large)
)
)
).all(ScreenValue::_4)
).y(ScreenValue::_2)
)
).wrap(Wrap::Wrap).group()
).top(ScreenValue::_6)
).push(
Padding(Flex(
SpaceBetween(
Div().vanish()
.push(
Text("Content").xl().bold().color(&Gray::_300)
)
.push(
Flex(
Div().vanish()
.push_if(!systemd_units.is_empty(), || {
InfoCard(
Div().vanish()
.push(
CardTitle("Systemd Units")
)
.push(
ListElements(&systemd_units)
)
)
})
.push_if(!pacman_hooks.is_empty(), || {
InfoCard(
Div().vanish()
.push(CardTitle("Pacman Hooks"))
.push(
Paragraph(
SpaceBetween(
Div().vanish()
.push_for_each(&pacman_hooks, |hook: &(String, String)| {
Div().vanish()
.push(
Text(&hook.0).xl().semibold().color(&Gray::_300)
)
.push(
Padding(Rounded(Background(Gray::_700, Code(&hook.1).sm().color(&Gray::_100))).size(Size::Large)).all(ScreenValue::_4)
)
})
.push(
html! {
@for unit in &systemd_units {
li { (unit) }
}
})
).y(ScreenValue::_1)
).list_style(ListStyle::Disc).color(&Gray::_300)
)
)
})
.push_if(!binaries.is_empty(),
|| InfoCard(
Div().vanish()
.push(
CardTitle("Binaries")
).push(
ListElements(&binaries)
)
)
)
.push(
InfoCard(
Div().vanish().push(
CardTitle("Package Files")
)
.push(
ListElements(&pkg.file_list())
)
)
)
).group().wrap(Wrap::Wrap)
)
).y(ScreenValue::_2)
).wrap(Wrap::Wrap).group()).top(ScreenValue::_6)
).push_some(install_script.as_ref(), |install_script: &String| {
SpaceBetween(
Margin(
Div()
.push(Text("Install Script")._3xl().semibold().color(&Gray::_300).indentation(ScreenValue::_2))
.push(Rounded(Padding(Background(Gray::_700,
Code(install_script.trim())
.color(&Gray::_100).sm()
.indentation(ScreenValue::_0)
)).all(ScreenValue::_2)).size(Size::Large))
).top(ScreenValue::_6)
).y(ScreenValue::_4)
}).render();
Some(render(content, pkg_name, ctx).await)
}
pub fn arch_card(a: &str, repo_name: &str, current: bool) -> PreEscaped<String> {
let url = if current {
&format!("/{repo_name}")
} else {
&format!("/{repo_name}?arch={a}")
};
let link = Link(&url, Text(&a).sm().medium().color(&Gray::_300)).use_htmx();
Rounded(
Padding(if current {
Background(Blue::_500, link)
} else {
Background(Gray::_700, link)
})
.x(ScreenValue::_3)
.y(ScreenValue::_1),
)
.size(Size::Full)
.render()
}
#[get("/<repo>?<arch>")]
pub async fn repo_ui(
repo: &str,
ctx: RequestContext,
arch: Option<&str>,
config: &State<Config>,
) -> StringResponse {
let arch = arch.map(|x| Architecture::parse(x).unwrap_or(Architecture::any));
let repo = Repository::new(repo).unwrap();
let architectures: Vec<_> = repo.arch().into_iter().map(|x| x.to_string()).collect();
let packages = if let Some(arch) = arch.clone() {
repo.list_pkg_arch(arch)
} else {
repo.list_pkg()
};
let repo_info = Margin(
// Repository name and architectures
Flex(
Div()
.vanish()
.push(Text(&repo.name)._3xl().bold().color(&Gray::_100))
.push_if(config.is_mirrored_repo(&repo.name), || {
Background(
Blue::_500,
Rounded(
Margin(
Padding(Text("Mirrored").sm().medium().white())
.x(ScreenValue::_3)
.y(ScreenValue::_1),
)
.left(ScreenValue::_4),
)
.size(Size::Full),
)
})
.push(
Screen::medium(Margin(Nothing()).top(ScreenValue::_0)).on(Margin(
Flex(Div().vanish().push_for_each(&architectures, |a: &String| {
html! {
@if let Some(arch) = arch.as_ref() {
@if arch.to_string() == *a {
(arch_card(&a, &repo.name, true))
} @else {
(arch_card(&a, &repo.name, false))
}
} @else {
(arch_card(&a, &repo.name, false))
}
}
}))
.group()
.gap(ScreenValue::_2),
)
.left(ScreenValue::_4)
.top(ScreenValue::_2)),
),
)
.wrap(Wrap::Wrap)
.full_center()
.group(),
)
.bottom(ScreenValue::_6);
let package_list = SpaceBetween(Div().vanish().push_for_each(&packages, |pkg| {
Animated(
Flex(
Padding(
Hover(Background(Gray::_600, Nothing())).on(Background(
Gray::_700,
Rounded(Shadow::medium(
Link(
&format!("/{}/{pkg}", repo.name),
Div()
.vanish()
.push(Context(Sized(
ScreenValue::_10,
ScreenValue::_10,
Rounded(Background(
Blue::_500,
Flex(
Text(
&pkg.chars()
.next()
.unwrap_or_default()
.to_uppercase()
.to_string(),
)
.white()
.semibold(),
)
.full_center()
.group(),
))
.size(Size::Full),
)))
.push(Context(Width(
ScreenValue::fit,
Text(&pkg).medium().color(&Gray::_100),
)))
.push(Context(
Padding(
Flex(
Div()
.vanish()
.push(
Span("")._2xl().bold().color(&Slate::_300),
)
.push(
Span("Signed")
.sm()
.medium()
.color(&Slate::_300),
),
)
.items_center()
.gap(ScreenValue::_2)
.group(),
)
.right(ScreenValue::_4),
)),
)
.use_htmx(),
))
.size(Size::Large),
)),
)
.all(ScreenValue::_4),
)
.items_center()
.gap(ScreenValue::_4),
)
}))
.y(ScreenValue::_4);
let content = Div().vanish().push(repo_info).push(package_list).render();
render(content, &repo.name, ctx).await
}
pub fn build_info(key: String, value: String) -> PreEscaped<String> {
match key.as_str() {
"download_amount" => {
return key_value("Downloads".to_string(), value);
}
"pkgname" => {}
"xdata" => {}
"arch" => {}
"pkbase" => {}
"pkgver" => {}
"pkgdesc" => {
return key_value("Description".to_string(), value);
}
"packager" => {
return key_value("Packager".to_string(), value);
}
"url" => {
return Flex(
Div()
.vanish()
.push(Width(ScreenValue::_32, Span("Website: ").bold()))
.push(
Margin(Link(&value, Span(&value).color(&Blue::_400))).left(ScreenValue::_6),
),
)
.group()
.render();
}
"builddate" => {
let date = chrono::DateTime::from_timestamp(value.parse().unwrap(), 0).unwrap();
return key_value(
"Build at".to_string(),
date.format("%d.%m.%Y %H:%M").to_string(),
);
}
"depend" => {
return pkg_list_info("Depends on", &value);
}
"makedepend" => {
return pkg_list_info("Build Dependencies", &value);
}
"license" => {
return key_value("License".to_string(), value);
}
"size" => {
return key_value(
"Size".to_string(),
bytesize::to_string(value.parse().unwrap(), false),
);
}
_ => {
log::warn!("Unhandled PKGINFO {key} = {value}");
}
}
html! {}
}
pub fn key_value(key: String, value: String) -> PreEscaped<String> {
Flex(
Div()
.vanish()
.push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold()))
.push(Margin(Span(&value)).left(ScreenValue::_6)),
)
.items_center()
.group()
.render()
}
pub fn take_out<T>(v: &mut Vec<T>, f: impl Fn(&T) -> bool) -> (&mut Vec<T>, Option<T>) {
let mut index = -1;
for (i, e) in v.iter().enumerate() {
if f(e) {
index = i as i64;
}
}
if index != -1 {
let e = v.remove(index as usize);
return (v, Some(e));
}
(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) {
Margin(Link(&pkg_url, Text(&pkg).color(&Blue::_400)).use_htmx()).left(ScreenValue::_6)
} else {
Margin(Span(&pkg)).left(ScreenValue::_6)
}
});
html! {
(Flex(
Div().vanish()
.push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold()))
.push(Flex(
html! {
@for pkg in pkgs {
(pkg)
}
}
).group().wrap(Wrap::Wrap))
).items_center().group())
}
}
#[allow(non_snake_case)]
pub fn ListElements(el: &[String]) -> TextWidget {
Paragraph(
SpaceBetween(html! {
@for unit in el {
li { (unit) }
}
})
.y(ScreenValue::_1),
)
.list_style(ListStyle::Disc)
.color(&Gray::_300)
}
#[allow(non_snake_case)]
pub fn CardTitle(title: &str) -> PreEscaped<String> {
Context(Text(title).large().medium().color(&Gray::_100).underlined())
}
#[allow(non_snake_case)]
pub fn InfoCard<T: UIWidget + 'static>(inner: T) -> SpaceBetweenWidget {
SpaceBetween(
Padding(Background(
Gray::_800,
Shadow::large(
Rounded(Margin(FlexGrow(Strategy::Grow, inner)).all(ScreenValue::_2))
.size(Size::Large),
),
))
.all(ScreenValue::_4),
)
.y(ScreenValue::_2)
}

View file

@ -5,6 +5,7 @@ use based::ui::primitives::space::SpaceBetweenWidget;
use based::ui::primitives::text::{Code, TextWidget}; use based::ui::primitives::text::{Code, TextWidget};
use based::ui::{UIWidget, prelude::*}; use based::ui::{UIWidget, prelude::*};
use maud::{PreEscaped, Render, html}; use maud::{PreEscaped, Render, html};
use pacco::pkg::package::PackageMetaInfo;
use rocket::{State, get}; use rocket::{State, get};
use pacco::pkg::{Package, Repository, arch::Architecture, find_package_by_name}; use pacco::pkg::{Package, Repository, arch::Architecture, find_package_by_name};
@ -28,6 +29,8 @@ pub async fn pkg_ui(
pkg.rel = rel; pkg.rel = rel;
} }
let dl_count = pkg.download_amount().await;
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();
@ -132,6 +135,9 @@ pub async fn pkg_ui(
take_out(&mut pkginfo, |x| x.0 == "size").1, take_out(&mut pkginfo, |x| x.0 == "size").1,
|x: (String, String)| build_info(x.0, x.1), |x: (String, String)| build_info(x.0, x.1),
) )
.push(
build_info("download_amount".to_string(), dl_count.to_string())
)
.push_for_each(&pkginfo, |(key, val)| { .push_for_each(&pkginfo, |(key, val)| {
build_info(key.clone(), val.clone()) build_info(key.clone(), val.clone())
}), }),
@ -450,6 +456,9 @@ pub async fn pkg_ui(
pub fn build_info(key: String, value: String) -> PreEscaped<String> { pub fn build_info(key: String, value: String) -> PreEscaped<String> {
match key.as_str() { match key.as_str() {
"download_amount" => {
return key_value("Downloads".to_string(), value);
}
"pkgname" => {} "pkgname" => {}
"pkgbase" => {} "pkgbase" => {}
"xdata" => {} "xdata" => {}