diff --git a/Dockerfile b/Dockerfile index 65aa0fd..a953ebc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,9 +27,9 @@ FROM archlinux # Copy the built binary from the previous stage COPY --from=builder /app/target/release/untitled /usr/local/bin/untitled -RUN echo " \ +RUN echo $'\ [extra]\ -Include = /etc/pacman.d/mirrorlist" >> /etc/pacman.conf +Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf RUN pacman -Syyu --noconfirm RUN pacman-key --init && pacman-key --populate diff --git a/backend/scripts/makepkg b/backend/scripts/makepkg index 80c5603..2ca53f0 100755 --- a/backend/scripts/makepkg +++ b/backend/scripts/makepkg @@ -1188,8 +1188,8 @@ fi if (( ! INFAKEROOT )); then if (( EUID == 0 )); then - error "$(gettext "Running %s as root is not allowed as it can cause permanent,\n\ -catastrophic damage to your system.")" "makepkg" + : #error "$(gettext "Running %s as root is not allowed as it can cause permanent,\n\ +#catastrophic damage to your system.")" "makepkg" #exit $E_ROOT fi else diff --git a/backend/src/api/list.rs b/backend/src/api/list.rs index 393dfd3..7276dc2 100644 --- a/backend/src/api/list.rs +++ b/backend/src/api/list.rs @@ -9,9 +9,9 @@ use rocket::serde::{Deserialize, Serialize}; use rocket::{get, State}; use rocket_okapi::okapi::schemars; use rocket_okapi::{openapi, JsonSchema}; -use sea_orm::PaginatorTrait; use sea_orm::{ColumnTrait, QueryFilter}; use sea_orm::{DatabaseConnection, EntityTrait, FromQueryResult, QuerySelect, RelationTrait}; +use sea_orm::{Order, PaginatorTrait, QueryOrder}; #[derive(Serialize, JsonSchema)] #[serde(crate = "rocket::serde")] @@ -43,8 +43,11 @@ pub async fn search(query: &str) -> Result>, String> { pub struct ListPackageModel { id: i32, name: String, - count: i32, status: i32, + outofdate: bool, + latest_version: String, + latest_version_id: i32, + latest_aur_version: String, } #[openapi(tag = "test")] @@ -55,13 +58,15 @@ pub async fn package_list( let db = db as &DatabaseConnection; let all: Vec = Packages::find() - .join_rev(JoinType::InnerJoin, versions::Relation::Packages.def()) + .join_rev(JoinType::InnerJoin, versions::Relation::LatestPackage.def()) .select_only() - .column_as(versions::Column::Id.count(), "count") .column(packages::Column::Name) .column(packages::Column::Id) .column(packages::Column::Status) - .group_by(packages::Column::Name) + .column_as(packages::Column::OutOfDate, "outofdate") + .column_as(packages::Column::LatestAurVersion, "latest_aur_version") + .column_as(versions::Column::Version, "latest_version") + .column_as(packages::Column::LatestVersionId, "latest_version_id") .into_model::() .all(db) .await @@ -146,6 +151,8 @@ pub struct ListBuildsModel { pkg_name: String, version: String, status: i32, + start_time: Option, + end_time: Option, } #[openapi(tag = "test")] @@ -165,6 +172,9 @@ pub async fn list_builds( .column(builds::Column::Status) .column_as(packages::Column::Name, "pkg_name") .column(versions::Column::Version) + .column(builds::Column::EndTime) + .column(builds::Column::StartTime) + .order_by(builds::Column::StartTime, Order::Desc) .limit(limit); let build = match pkgid { @@ -197,6 +207,8 @@ pub async fn get_build( .column(builds::Column::Status) .column_as(packages::Column::Name, "pkg_name") .column(versions::Column::Version) + .column(builds::Column::EndTime) + .column(builds::Column::StartTime) .into_model::() .one(db) .await diff --git a/backend/src/builder/builder.rs b/backend/src/builder/builder.rs index 085631a..3f077f1 100644 --- a/backend/src/builder/builder.rs +++ b/backend/src/builder/builder.rs @@ -5,6 +5,7 @@ use crate::repo::repo::add_pkg; use anyhow::anyhow; use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set}; use std::ops::Add; +use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::broadcast; use tokio::sync::broadcast::error::RecvError; use tokio::sync::broadcast::Sender; @@ -16,12 +17,17 @@ pub async fn init(db: DatabaseConnection, tx: Sender) { // add a package to parallel build Action::Build(name, version, url, mut version_model) => { let db = db.clone(); - let build = builds::ActiveModel { pkg_id: version_model.package_id.clone(), version_id: version_model.id.clone(), ouput: Set(None), status: Set(Some(0)), + start_time: Set(Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as u32, + )), ..Default::default() }; let mut new_build = build.save(&db).await.unwrap(); @@ -62,6 +68,8 @@ pub async fn init(db: DatabaseConnection, tx: Sender) { let _ = set_pkg_status( &db, version_model.package_id.clone().unwrap(), + version_model.id.clone().unwrap(), + Some(false), 1, ) .await; @@ -70,18 +78,32 @@ pub async fn init(db: DatabaseConnection, tx: Sender) { let _ = version_model.update(&db).await; new_build.status = Set(Some(1)); + new_build.end_time = Set(Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as u32, + )); let _ = new_build.update(&db).await; } Err(e) => { let _ = set_pkg_status( &db, version_model.package_id.clone().unwrap(), + version_model.id.clone().unwrap(), + None, 2, ) .await; let _ = version_model.update(&db).await; new_build.status = Set(Some(2)); + new_build.end_time = Set(Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as u32, + )); let _ = new_build.update(&db).await; println!("Error: {e}") @@ -98,6 +120,8 @@ pub async fn init(db: DatabaseConnection, tx: Sender) { async fn set_pkg_status( db: &DatabaseConnection, package_id: i32, + version_id: i32, + outofdate: Option, status: i32, ) -> anyhow::Result<()> { let mut pkg: packages::ActiveModel = Packages::find_by_id(package_id) @@ -107,6 +131,10 @@ async fn set_pkg_status( .into(); pkg.status = Set(status); + pkg.latest_version_id = Set(Some(version_id)); + if outofdate.is_some() { + pkg.out_of_date = Set(outofdate.unwrap() as i32) + } pkg.update(db).await?; Ok(()) } diff --git a/backend/src/db/builds.rs b/backend/src/db/builds.rs index 2ea7feb..5fcfdb6 100644 --- a/backend/src/db/builds.rs +++ b/backend/src/db/builds.rs @@ -14,6 +14,8 @@ pub struct Model { pub version_id: i32, pub ouput: Option, pub status: Option, + pub start_time: Option, + pub end_time: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/backend/src/db/migration/create.rs b/backend/src/db/migration/create.rs index e33ef8a..aa40220 100644 --- a/backend/src/db/migration/create.rs +++ b/backend/src/db/migration/create.rs @@ -19,7 +19,9 @@ create table builds pkg_id integer not null, version_id integer not null, ouput TEXT, - status integer + status integer, + start_time INTEGER, + end_time integer ); create table packages @@ -27,7 +29,12 @@ create table packages id integer not null primary key autoincrement, name text not null, - status integer default 0 not null + status integer default 0 not null, + out_of_date INTEGER default 0 not null, + latest_version_id integer + constraint packages_versions_id_fk + references versions, + latest_aur_version TEXT ); create table status diff --git a/backend/src/db/packages.rs b/backend/src/db/packages.rs index 7b72f74..2a962b8 100644 --- a/backend/src/db/packages.rs +++ b/backend/src/db/packages.rs @@ -12,6 +12,9 @@ pub struct Model { pub id: i32, pub name: String, pub status: i32, + pub out_of_date: i32, + pub latest_version_id: Option, + pub latest_aur_version: Option, } impl ActiveModelBehavior for ActiveModel {} @@ -22,6 +25,8 @@ pub enum Relation { Versions, #[sea_orm(has_many = "super::builds::Entity")] Builds, + #[sea_orm(has_one = "super::versions::Entity")] + LatestVersion, } impl Related for Entity { @@ -30,8 +35,14 @@ impl Related for Entity { } } +// impl Related for Entity { +// fn to() -> RelationDef { +// Relation::LatestVersion.def() +// } +// } + impl Related for crate::db::versions::Entity { fn to() -> RelationDef { - crate::db::versions::Relation::Builds.def() + Relation::Builds.def() } } diff --git a/backend/src/db/versions.rs b/backend/src/db/versions.rs index e7f049c..7ba5b4f 100644 --- a/backend/src/db/versions.rs +++ b/backend/src/db/versions.rs @@ -23,6 +23,12 @@ pub enum Relation { to = "super::packages::Column::Id" )] Packages, + #[sea_orm( + belongs_to = "super::packages::Entity", + from = "Column::Id", + to = "super::packages::Column::LatestVersionId" + )] + LatestPackage, #[sea_orm(has_many = "super::builds::Entity")] Builds, } diff --git a/backend/src/main.rs b/backend/src/main.rs index 2a08873..3d9c655 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -4,6 +4,7 @@ mod builder; mod db; mod pkgbuild; mod repo; +mod scheduler; mod utils; use crate::api::backend; @@ -11,6 +12,7 @@ use crate::api::backend; use crate::api::embed::CustomHandler; use crate::builder::types::Action; use crate::db::migration::Migrator; +use crate::scheduler::aur_version_update::start_aur_version_checking; use rocket::config::Config; use rocket::fs::FileServer; use rocket::futures::future::join_all; @@ -48,6 +50,8 @@ fn main() { builder::builder::init(db2, tx2).await; }); + start_aur_version_checking(db.clone()); + let backend_handle = tokio::spawn(async { let mut config = Config::default(); config.address = "0.0.0.0".parse().unwrap(); diff --git a/backend/src/scheduler/aur_version_update.rs b/backend/src/scheduler/aur_version_update.rs new file mode 100644 index 0000000..65760e8 --- /dev/null +++ b/backend/src/scheduler/aur_version_update.rs @@ -0,0 +1,55 @@ +use crate::db::packages; +use crate::db::prelude::Packages; +use anyhow::anyhow; +use aur_rs::{Package, Request}; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; +use std::time::Duration; +use tokio::time::sleep; + +pub fn start_aur_version_checking(db: DatabaseConnection) { + tokio::spawn(async move { + sleep(Duration::from_secs(10)).await; + loop { + println!("performing aur version checks"); + match aur_check_versions(db.clone()).await { + Ok(_) => {} + Err(e) => { + println!("Failed to perform aur version check: {e}") + } + } + sleep(Duration::from_secs(3600)).await; + } + }); +} + +async fn aur_check_versions(db: DatabaseConnection) -> anyhow::Result<()> { + let packages = Packages::find().all(&db).await?; + let names: Vec<&str> = packages.iter().map(|x| x.name.as_str()).collect(); + + let request = Request::default(); + let response = request.search_multi_info_by_names(names.as_slice()).await; + + let results: Vec = response + .map_err(|_| anyhow!("couldn't download version update"))? + .results; + + if results.len() != packages.len() { + println!("Package nr in repo and aur api response has different size"); + } + + for package in packages { + match results.iter().find(|x1| x1.name == package.name) { + None => { + println!("Couldn't find {} in AUR response", package.name) + } + Some(result) => { + let mut package: packages::ActiveModel = package.into(); + + package.latest_aur_version = Set(Some(result.version.clone())); + let _ = package.update(&db).await; + } + } + } + Ok(()) +} diff --git a/backend/src/scheduler/mod.rs b/backend/src/scheduler/mod.rs new file mode 100644 index 0000000..6e40c2f --- /dev/null +++ b/backend/src/scheduler/mod.rs @@ -0,0 +1 @@ +pub mod aur_version_update; diff --git a/frontend/lib/components/dashboard/your_packages.dart b/frontend/lib/components/dashboard/your_packages.dart index 14d5408..d3eb6b6 100644 --- a/frontend/lib/components/dashboard/your_packages.dart +++ b/frontend/lib/components/dashboard/your_packages.dart @@ -58,7 +58,10 @@ class _YourPackagesState extends State { label: Text("Package Name"), ), DataColumn( - label: Text("Number of versions"), + label: Text("Version"), + ), + DataColumn( + label: Text("Up-To-Date"), ), DataColumn( label: Text("Status"), @@ -85,14 +88,25 @@ class _YourPackagesState extends State { cells: [ DataCell(Text(package.id.toString())), DataCell(Text(package.name)), - DataCell(Text(package.count.toString())), + DataCell(Text(package.latest_version.toString())), + DataCell(IconButton( + icon: Icon( + package.outofdate ? Icons.update : Icons.verified, + color: package.outofdate ? Color(0xFF6B43A4) : Color(0xFF0A6900), + ), + onPressed: package.outofdate + ? () { + // todo open build info with logs + } + : null, + )), DataCell(IconButton( icon: Icon( switchSuccessIcon(package.status), color: switchSuccessColor(package.status), ), onPressed: () { - // todo open build info with logs + //context.push("/build/${package.latest_version_id}"); }, )), DataCell( diff --git a/frontend/lib/models/build.dart b/frontend/lib/models/build.dart index 6b472ce..bf839aa 100644 --- a/frontend/lib/models/build.dart +++ b/frontend/lib/models/build.dart @@ -3,17 +3,22 @@ class Build { final String pkg_name; final String version; final int status; + final int? start_time, end_time; Build( {required this.id, required this.pkg_name, required this.version, + required this.start_time, + required this.end_time, required this.status}); factory Build.fromJson(Map json) { return Build( id: json["id"] as int, status: json["status"] as int, + start_time: json["start_time"] as int?, + end_time: json["end_time"] as int?, pkg_name: json["pkg_name"] as String, version: json["version"] as String, ); diff --git a/frontend/lib/models/package.dart b/frontend/lib/models/package.dart index 917b243..d386754 100644 --- a/frontend/lib/models/package.dart +++ b/frontend/lib/models/package.dart @@ -1,21 +1,28 @@ class Package { - final int id; + final int id, latest_version_id; final String name; - final int count; + final bool outofdate; final int status; + final String latest_version, latest_aur_version; Package( {required this.id, + required this.latest_version_id, required this.name, - required this.count, - required this.status}); + required this.status, + required this.latest_version, + required this.latest_aur_version, + required this.outofdate}); factory Package.fromJson(Map json) { return Package( id: json["id"] as int, - count: json["count"] as int, + outofdate: json["outofdate"] as bool, status: json["status"] as int, name: json["name"] as String, + latest_version: json["latest_version"] as String, + latest_version_id: json["latest_version_id"] as int, + latest_aur_version: json["latest_aur_version"] as String, ); } } diff --git a/frontend/lib/screens/build_screen.dart b/frontend/lib/screens/build_screen.dart index 03d6242..0d92879 100644 --- a/frontend/lib/screens/build_screen.dart +++ b/frontend/lib/screens/build_screen.dart @@ -1,14 +1,11 @@ -import 'dart:async'; - -import 'package:aurcache/api/builds.dart'; import 'package:aurcache/components/build_output.dart'; import 'package:aurcache/models/build.dart'; import 'package:aurcache/components/api/APIBuilder.dart'; import 'package:aurcache/providers/build_provider.dart'; +import 'package:aurcache/utils/time_formatter.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import '../api/API.dart'; import '../components/dashboard/your_packages.dart'; class BuildScreen extends StatefulWidget { @@ -29,6 +26,9 @@ class _BuildScreenState extends State { interval: const Duration(seconds: 10), onLoad: () => const Text("no data"), onData: (buildData) { + final start_time = DateTime.fromMillisecondsSinceEpoch( + (buildData.start_time ?? 0) * 1000); + return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, @@ -58,7 +58,7 @@ class _BuildScreenState extends State { const SizedBox( width: 10, ), - const Text("triggered 2 months ago") + Text("triggered ${start_time.readableDuration()}") ], ), const SizedBox( diff --git a/frontend/lib/screens/dashboard_screen.dart b/frontend/lib/screens/dashboard_screen.dart index 4a467cc..f0e7213 100644 --- a/frontend/lib/screens/dashboard_screen.dart +++ b/frontend/lib/screens/dashboard_screen.dart @@ -23,6 +23,7 @@ class _DashboardScreenState extends State { @override Widget build(BuildContext context) { return APIBuilder( + interval: const Duration(seconds: 10), onData: (stats) { return SafeArea( child: SingleChildScrollView( diff --git a/frontend/lib/utils/time_formatter.dart b/frontend/lib/utils/time_formatter.dart new file mode 100644 index 0000000..14e714c --- /dev/null +++ b/frontend/lib/utils/time_formatter.dart @@ -0,0 +1,20 @@ +extension TimeFormatter on DateTime { + String readableDuration() { + final now = DateTime.now(); + final duration = now.difference(this); + + if (duration.inSeconds < 60) { + return '${duration.inSeconds} seconds ago'; + } else if (duration.inMinutes < 60) { + return '${duration.inMinutes} minutes ago'; + } else if (duration.inHours < 24) { + return '${duration.inHours} hours ago'; + } else if (duration.inDays < 30) { + return '${duration.inDays} days ago'; + } else if ((duration.inDays / 30) < 12) { + return '${duration.inDays ~/ 30} months ago'; + } else { + return '${duration.inDays ~/ 365} years ago'; + } + } +}