Compare commits

...

3 Commits

Author SHA1 Message Date
b6adc918c6 Unify database structs 2025-02-09 21:56:06 +01:00
3d8867d218 Connect to db via storage module 2025-02-09 21:24:57 +01:00
fa489bf7bc Move flake metadata specification to seperate file 2025-02-09 21:17:13 +01:00
6 changed files with 216 additions and 209 deletions

View File

@ -1,5 +1,4 @@
use anyhow::{
anyhow,
Context,
Result,
};
@ -9,157 +8,18 @@ use chrono::{
use clap::{
Parser,
};
use serde::{
Deserialize,
use flake_tracker::{
flake::{
FlakeLocksNodeInputs,
FlakeMetadata,
FlakeUri,
},
storage::{
Storage,
},
};
use sqlx::{
sqlite::SqlitePoolOptions,
};
use std::collections::HashMap;
use std::process::Command;
#[derive(Deserialize, Debug)]
struct FlakeMetadata {
lastModified: i64,
locked: FlakeLockedInfo,
locks: FlakeLocks,
original: FlakeSource,
path: String,
resolved: FlakeSource,
revision: String,
}
#[derive(Deserialize, Debug, Clone)]
struct FlakeSource {
owner: Option<String>,
repo: Option<String>,
r#ref: Option<String>,
rev: Option<String>,
r#type: String,
url: Option<String>,
}
fn assemble_flake_git_uri(flake_source: &FlakeSource) -> Result<String> {
let mut out = String::new();
out.push_str("git+");
out.push_str(&flake_source.url.clone()
.context("Flake git uri does not contain an url")?);
let mut query: Vec<String> = Vec::new();
if let Some(r#ref) = &flake_source.r#ref {
query.push(format!("ref={}", r#ref));
}
if let Some(rev) = &flake_source.rev {
query.push(format!("rev={}", rev));
}
if query.len() > 0 {
out.push_str("?");
out.push_str(&query.join("&"));
}
Ok(out)
}
fn assemble_flake_github_uri(flake_source: &FlakeSource) -> Result<String> {
let mut out = String::new();
out.push_str("github:");
out.push_str(&flake_source.owner.clone()
.context("Flake github uri does not contain an owner")?);
out.push_str("/");
out.push_str(&flake_source.repo.clone()
.context("Flake github uri does not contain an repo")?);
if let Some(rev) = &flake_source.rev {
out.push_str("/");
out.push_str(&rev);
}
Ok(out)
}
fn assemble_flake_tarball_uri(flake_source: &FlakeSource) -> Result<String> {
let mut out = String::new();
out.push_str(&flake_source.url.clone()
.context("Flake tarball uri does not contain an url")?);
if let Some(rev) = &flake_source.rev {
out.push_str("?rev=");
out.push_str(rev);
}
Ok(out)
}
trait FlakeUri {
fn flake_uri(&self) -> Result<String>;
}
impl FlakeUri for FlakeSource {
fn flake_uri(&self) -> Result<String> {
match self.r#type.as_str() {
"git" => assemble_flake_git_uri(self),
"github" => assemble_flake_github_uri(self),
"tarball" => assemble_flake_tarball_uri(self),
other => Err(anyhow!("Unsupported flake uri type {}", other)),
}
}
}
#[derive(Deserialize, Debug, Clone)]
struct FlakeLockedInfo {
lastModified: i64,
narHash: String,
owner: Option<String>,
repo: Option<String>,
r#ref: Option<String>,
rev: Option<String>,
r#type: String,
url: Option<String>,
}
impl FlakeUri for FlakeLockedInfo {
fn flake_uri(&self) -> Result<String> {
Into::<FlakeSource>::into(self).flake_uri()
}
}
impl Into<FlakeSource> for &FlakeLockedInfo {
fn into(self) -> FlakeSource {
FlakeSource {
owner: self.owner.clone(),
repo: self.repo.clone(),
r#ref: self.r#ref.clone(),
rev: self.rev.clone(),
r#type: self.r#type.clone(),
url: self.url.clone(),
}
}
}
#[derive(Deserialize, Debug)]
struct FlakeLocks {
nodes: HashMap<String, FlakeLocksNode>,
root: String,
version: u64,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum FlakeLocksNodeInputs {
String(String),
Array(Vec<String>),
}
#[derive(Deserialize, Debug)]
struct FlakeLocksNode {
inputs: Option<HashMap<String, FlakeLocksNodeInputs>>,
locked: Option<FlakeLockedInfo>,
original: Option<FlakeSource>,
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
@ -172,7 +32,10 @@ async fn main() -> Result<()> {
let scan_time = Utc::now().timestamp();
let db = SqlitePoolOptions::new().connect("sqlite://flake-tracker.db").await?;
let storage = Storage::connect("sqlite://flake-tracker.db")
.await
.context("Failed to connect to database")?;
let flake_metadata_raw = Command::new("nix")
.arg("flake")
@ -200,7 +63,7 @@ async fn main() -> Result<()> {
.bind(&flake_metadata.locked.narHash)
.bind(&flake_metadata.locked.lastModified)
.bind(&scan_time)
.execute(&db).await?;
.execute(&storage.db).await?;
let locks_root_name = &flake_metadata.locks.root;
let locks_root_node = flake_metadata.locks.nodes.get(locks_root_name)
@ -226,7 +89,7 @@ async fn main() -> Result<()> {
.bind(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?)
.bind(locks_input_node.locked.clone().context("Unexpected missing lock")?.narHash)
.bind(locks_input_node.locked.clone().context("Unexpected missing lock")?.lastModified)
.execute(&db).await?;
.execute(&storage.db).await?;
sqlx::query("INSERT INTO revisions (revision_uri, flake_uri)
VALUES (?, ?)
@ -234,7 +97,7 @@ async fn main() -> Result<()> {
")
.bind(locks_input_node.locked.clone().context("Unexpected missing lock")?.flake_uri()?)
.bind(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?)
.execute(&db).await?;
.execute(&storage.db).await?;
}
}

152
src/flake.rs Normal file
View File

@ -0,0 +1,152 @@
use anyhow::{
anyhow,
Context,
Result,
};
use serde::{
Deserialize,
};
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
pub struct FlakeMetadata {
pub lastModified: i64,
pub locked: FlakeLockedInfo,
pub locks: FlakeLocks,
pub original: FlakeSource,
pub path: String,
pub resolved: FlakeSource,
pub revision: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FlakeSource {
pub owner: Option<String>,
pub repo: Option<String>,
pub r#ref: Option<String>,
pub rev: Option<String>,
pub r#type: String,
pub url: Option<String>,
}
pub fn assemble_flake_git_uri(flake_source: &FlakeSource) -> Result<String> {
let mut out = String::new();
out.push_str("git+");
out.push_str(&flake_source.url.clone()
.context("Flake git uri does not contain an url")?);
let mut query: Vec<String> = Vec::new();
if let Some(r#ref) = &flake_source.r#ref {
query.push(format!("ref={}", r#ref));
}
if let Some(rev) = &flake_source.rev {
query.push(format!("rev={}", rev));
}
if query.len() > 0 {
out.push_str("?");
out.push_str(&query.join("&"));
}
Ok(out)
}
pub fn assemble_flake_github_uri(flake_source: &FlakeSource) -> Result<String> {
let mut out = String::new();
out.push_str("github:");
out.push_str(&flake_source.owner.clone()
.context("Flake github uri does not contain an owner")?);
out.push_str("/");
out.push_str(&flake_source.repo.clone()
.context("Flake github uri does not contain an repo")?);
if let Some(rev) = &flake_source.rev {
out.push_str("/");
out.push_str(&rev);
}
Ok(out)
}
pub fn assemble_flake_tarball_uri(flake_source: &FlakeSource) -> Result<String> {
let mut out = String::new();
out.push_str(&flake_source.url.clone()
.context("Flake tarball uri does not contain an url")?);
if let Some(rev) = &flake_source.rev {
out.push_str("?rev=");
out.push_str(rev);
}
Ok(out)
}
pub trait FlakeUri {
fn flake_uri(&self) -> Result<String>;
}
impl FlakeUri for FlakeSource {
fn flake_uri(&self) -> Result<String> {
match self.r#type.as_str() {
"git" => assemble_flake_git_uri(self),
"github" => assemble_flake_github_uri(self),
"tarball" => assemble_flake_tarball_uri(self),
other => Err(anyhow!("Unsupported flake uri type {}", other)),
}
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct FlakeLockedInfo {
pub lastModified: i64,
pub narHash: String,
pub owner: Option<String>,
pub repo: Option<String>,
pub r#ref: Option<String>,
pub rev: Option<String>,
pub r#type: String,
pub url: Option<String>,
}
impl FlakeUri for FlakeLockedInfo {
fn flake_uri(&self) -> Result<String> {
Into::<FlakeSource>::into(self).flake_uri()
}
}
impl Into<FlakeSource> for &FlakeLockedInfo {
fn into(self) -> FlakeSource {
FlakeSource {
owner: self.owner.clone(),
repo: self.repo.clone(),
r#ref: self.r#ref.clone(),
rev: self.rev.clone(),
r#type: self.r#type.clone(),
url: self.url.clone(),
}
}
}
#[derive(Deserialize, Debug)]
pub struct FlakeLocks {
pub nodes: HashMap<String, FlakeLocksNode>,
pub root: String,
pub version: u64,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum FlakeLocksNodeInputs {
String(String),
Array(Vec<String>),
}
#[derive(Deserialize, Debug)]
pub struct FlakeLocksNode {
pub inputs: Option<HashMap<String, FlakeLocksNodeInputs>>,
pub locked: Option<FlakeLockedInfo>,
pub original: Option<FlakeSource>,
}

View File

@ -1,3 +1,4 @@
pub mod flake;
pub mod storage;
pub mod templates;
pub mod utils;

View File

@ -28,7 +28,7 @@ impl Storage {
})
}
pub async fn flakes(&self) -> Result<Vec<FlakeUri>> {
pub async fn flakes(&self) -> Result<Vec<FlakeRow>> {
sqlx::query_as("
SELECT
flake_uri
@ -40,11 +40,15 @@ impl Storage {
.context("Failed to fetch data from database")
}
pub async fn revisions_from_flake(&self, uri: &str) -> Result<Vec<RevisionListModel>> {
pub async fn revisions_from_flake(&self, uri: &str) -> Result<Vec<RevisionRow>> {
sqlx::query_as("
SELECT
revision_uri,
last_modified
flake_uri,
nix_store_path,
nar_hash,
last_modified,
tracker_last_scanned
FROM revisions
WHERE flake_uri = ?
ORDER BY last_modified DESC
@ -55,7 +59,7 @@ impl Storage {
.context("Failed to fetch data from database")
}
pub async fn inputs_for_revision(&self, revision_uri: &str) -> Result<Vec<InputModel>> {
pub async fn inputs_for_revision(&self, revision_uri: &str) -> Result<Vec<InputRow>> {
sqlx::query_as("
SELECT
revision_uri,
@ -74,7 +78,7 @@ impl Storage {
.context("Failed to fetch data from database")
}
pub async fn input_of_for_revision(&self, revision_uri: &str) -> Result<Vec<InputModel>> {
pub async fn input_of_for_revision(&self, revision_uri: &str) -> Result<Vec<InputRow>> {
sqlx::query_as("
SELECT
revision_uri,
@ -93,14 +97,17 @@ impl Storage {
.context("Failed to fetch data from database")
}
pub async fn current_inputs_for_flake(&self, flake_uri: &str) -> Result<Vec<InputForFlakeRow>> {
pub async fn current_inputs_for_flake(&self, flake_uri: &str) -> Result<Vec<InputRow>> {
sqlx::query_as("
SELECT
revisions.revision_uri,
MAX(revisions.last_modified) AS last_modified,
inputs.input_name,
inputs.locked_flake_uri
inputs.locked_revision_uri,
inputs.locked_flake_uri,
inputs.locked_nar_hash,
inputs.last_modified,
MAX(revisions.last_modified)
FROM
revisions
LEFT JOIN
@ -120,57 +127,30 @@ impl Storage {
}
#[derive(FromRow)]
pub struct FlakeRevisionRow {
pub struct RevisionRow {
pub revision_uri: String,
pub flake_uri: Option<String>,
pub nix_store_path: Option<String>,
pub nar_hash: Option<String>,
pub last_modified: Option<i64>,
pub tracker_last_scanned: Option<i64>,
}
#[derive(FromRow)]
pub struct InputForFlakeRow {
pub revision_uri: String,
pub last_modified: Option<i64>,
pub input_name: String,
pub locked_flake_uri: Option<String>,
impl RevisionRow {
pub fn revision_link(&self) -> String {
format!("/r/{}", urlencode(&self.revision_uri))
}
impl InputForFlakeRow {
pub fn locked_flake_link(&self ) -> String {
match &self.locked_flake_uri {
Some(locked_flake_uri) => format!("/f/{}", urlencode(&locked_flake_uri)),
pub fn flake_link(&self) -> String {
match &self.flake_uri {
Some(flake_uri) => format!("/f/{}", urlencode(&flake_uri)),
None => String::from("#"),
}
}
}
#[derive(FromRow)]
pub struct RevisionListModel {
pub revision_uri: String,
pub last_modified: i64,
}
impl RevisionListModel {
pub fn revision_link(&self ) -> String {
format!("/r/{}", urlencode(&self.revision_uri))
}
}
#[derive(FromRow)]
pub struct FlakeUri {
pub flake_uri: String,
}
impl FlakeUri {
pub fn flake_link(&self ) -> String {
format!("/f/{}", urlencode(&self.flake_uri))
}
}
#[derive(FromRow)]
pub struct InputModel {
pub struct InputRow {
pub revision_uri: String,
pub input_name: String,
pub locked_revision_uri: Option<String>,
@ -179,7 +159,7 @@ pub struct InputModel {
pub last_modified: Option<i64>,
}
impl InputModel {
impl InputRow {
pub fn revision_link(&self) -> String {
format!("/r/{}", urlencode(&self.revision_uri))
}
@ -198,3 +178,15 @@ impl InputModel {
}
}
}
#[derive(FromRow)]
pub struct FlakeRow {
pub flake_uri: String,
}
impl FlakeRow {
pub fn flake_link(&self ) -> String {
format!("/f/{}", urlencode(&self.flake_uri))
}
}

View File

@ -3,10 +3,9 @@ use askama::{
};
use crate::{
storage::{
FlakeUri,
InputModel,
InputForFlakeRow,
RevisionListModel,
FlakeRow,
InputRow,
RevisionRow,
},
};
@ -18,21 +17,21 @@ pub struct IndexTemplate {
#[derive(Template)]
#[template(path = "flakes.html")]
pub struct FlakesTemplate {
pub flakes: Vec<FlakeUri>,
pub flakes: Vec<FlakeRow>,
}
#[derive(Template)]
#[template(path = "flake.html")]
pub struct FlakeTemplate {
pub uri: String,
pub revisions: Vec<RevisionListModel>,
pub current_inputs: Vec<InputForFlakeRow>,
pub revisions: Vec<RevisionRow>,
pub current_inputs: Vec<InputRow>,
}
#[derive(Template)]
#[template(path = "revision.html")]
pub struct RevisionTemplate {
pub revision_uri: String,
pub inputs: Vec<InputModel>,
pub input_of: Vec<InputModel>,
pub inputs: Vec<InputRow>,
pub input_of: Vec<InputRow>,
}

View File

@ -11,7 +11,7 @@
<ul>
{% for revision in revisions %}
<li><a href="{{ revision.revision_link() }}">{{ revision.revision_uri }}</a> ({{ revision.last_modified }})</li>
<li><a href="{{ revision.revision_link() }}">{{ revision.revision_uri }}</a> {% match revision.last_modified %}{% when Some with (last_modified) %}({{ last_modified }}){% when None %}{% endmatch %}</li>
{% endfor %}
</ul>