Compare commits

..

3 Commits

Author SHA1 Message Date
d2957d1799 Make all struct members snake case 2025-02-10 17:34:41 +01:00
f6733f86a7 Move flake scanning to module 2025-02-10 17:30:24 +01:00
7a4fbaf74a Move web router to separate module 2025-02-10 12:08:31 +01:00
6 changed files with 226 additions and 185 deletions

View File

@ -2,25 +2,17 @@ use anyhow::{
Context,
Result,
};
use chrono::{
Utc,
};
use clap::{
Parser,
};
use flake_tracker::{
flake::{
FlakeLocksNodeInputs,
FlakeMetadata,
FlakeUri,
scan::{
scan_flake,
},
storage::{
InputRow,
RevisionRow,
Storage,
},
};
use std::process::Command;
#[derive(Parser)]
#[command(version, about, long_about = None)]
@ -32,69 +24,11 @@ struct Cli {
async fn main() -> Result<()> {
let cli = Cli::parse();
let scan_time = Utc::now().timestamp();
let storage = Storage::connect("sqlite://flake-tracker.db")
.await
.context("Failed to connect to database")?;
let flake_metadata_raw = Command::new("nix")
.arg("flake")
.arg("metadata")
.arg("--json")
.arg(cli.flake_uri)
.output()
.context("Failed to fetch flake metadata")?;
let flake_metadata: FlakeMetadata = serde_json::from_slice(&flake_metadata_raw.stdout)
.context("Failed to parse flake metadata")?;
let revision_row = RevisionRow {
revision_uri: flake_metadata.locked.flake_uri()?.clone(),
flake_uri: Some(flake_metadata.resolved.flake_uri()?.clone()),
nix_store_path: Some(flake_metadata.path.clone()),
nar_hash: Some(flake_metadata.locked.narHash.clone()),
last_modified: Some(flake_metadata.locked.lastModified.clone()),
tracker_last_scanned: Some(scan_time.clone()),
};
storage.set_revision(revision_row)
.await?;
let locks_root_name = &flake_metadata.locks.root;
let locks_root_node = flake_metadata.locks.nodes.get(locks_root_name)
.context("Failed to get locks root node")?;
for (input_name, locks_input_name) in locks_root_node.inputs.clone().context("No inputs found for flake")? {
if let FlakeLocksNodeInputs::String(locks_input_name) = locks_input_name {
let locks_input_node = flake_metadata.locks.nodes.get(&locks_input_name)
.context("Failed to find lock of input")?;
let input_row = InputRow {
revision_uri: flake_metadata.locked.flake_uri()?.clone(),
input_name: input_name.clone(),
locked_revision_uri: Some(locks_input_node.locked.clone().context("Unexpected missing lock")?.flake_uri()?),
locked_flake_uri: Some(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?),
locked_nar_hash: Some(locks_input_node.locked.clone().context("Unexpected missing lock")?.narHash),
last_modified: Some(locks_input_node.locked.clone().context("Unexpected missing lock")?.lastModified),
};
storage.set_input(input_row)
.await?;
let revision_row = RevisionRow {
revision_uri: locks_input_node.locked.clone().context("Unexpected missing lock")?.flake_uri()?.clone(),
flake_uri: Some(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?.clone()),
nix_store_path: None,
nar_hash: None,
last_modified: None,
tracker_last_scanned: None,
};
storage.set_revision_exist(revision_row)
.await?;
}
}
scan_flake(storage, &cli.flake_uri).await?;
Ok(())
}

View File

@ -1,82 +1,15 @@
use anyhow::{
Context,
};
use askama::Template;
use axum::{
extract::{
Path,
State,
},
http::{
StatusCode,
},
response::{
IntoResponse,
Response,
},
Router,
routing::{
get,
},
};
use flake_tracker::{
storage::{
Storage,
},
templates::{
FlakeListTemplate,
FlakeTemplate,
IndexTemplate,
RevisionTemplate,
web::{
make_router,
},
};
struct AppError(anyhow::Error);
impl std::fmt::Display for AppError {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, formatter)
}
}
impl std::fmt::Debug for AppError {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.0, formatter)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal server error:\n{:?}", self),
)
.into_response()
}
}
impl<E> From<E> for AppError where E: Into<anyhow::Error> {
fn from(err: E) -> Self {
Self(err.into())
}
}
fn render_template<T: Template>(t: &T) -> anyhow::Result<Response> {
let body = t.render()
.context("Failed to render template")?;
Ok((
[
("Content-Type", T::MIME_TYPE),
],
body,
).into_response())
}
#[derive(Clone)]
struct AppState {
storage: Storage,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@ -84,16 +17,8 @@ async fn main() -> anyhow::Result<()> {
.await
.context("Failed to connect to database")?;
let state = AppState {
storage,
};
let app = Router::new()
.route("/", get(route_index))
.route("/flakes", get(route_flakes))
.route("/f/{uri}", get(route_flake))
.route("/r/{revision_uri}", get(route_revision))
.with_state(state);
let app = make_router(storage)?;
let listener = tokio::net::TcpListener::bind("[::]:3000")
.await
@ -105,38 +30,3 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
async fn route_index() -> Result<impl IntoResponse, AppError> {
Ok(render_template(&IndexTemplate {})?)
}
async fn route_flakes(
State(state): State<AppState>,
) -> Result<impl IntoResponse, AppError> {
Ok(render_template(&FlakeListTemplate {
flakes: state.storage.flakes().await?,
})?)
}
async fn route_flake(
State(state): State<AppState>,
Path(uri): Path<String>,
) -> Result<impl IntoResponse, AppError> {
Ok(render_template(&FlakeTemplate {
uri: uri.clone(),
revisions: state.storage.revisions_from_flake(&uri).await?,
current_inputs: state.storage.current_inputs_for_flake(&uri).await?,
})?)
}
async fn route_revision(
State(state): State<AppState>,
Path(revision_uri): Path<String>,
) -> Result<impl IntoResponse, AppError> {
Ok(render_template(&RevisionTemplate {
revision_uri: revision_uri.clone(),
inputs: state.storage.inputs_for_revision(&revision_uri).await?,
input_of: state.storage.input_of_for_revision(&revision_uri).await?,
})?)
}

View File

@ -10,7 +10,8 @@ use std::collections::HashMap;
#[derive(Deserialize, Debug)]
pub struct FlakeMetadata {
pub lastModified: i64,
#[serde(rename = "lastModified")]
pub last_modified: i64,
pub locked: FlakeLockedInfo,
pub locks: FlakeLocks,
pub original: FlakeSource,
@ -100,8 +101,10 @@ impl FlakeUri for FlakeSource {
#[derive(Deserialize, Debug, Clone)]
pub struct FlakeLockedInfo {
pub lastModified: i64,
pub narHash: String,
#[serde(rename = "lastModified")]
pub last_modified: i64,
#[serde(rename = "narHash")]
pub nar_hash: String,
pub owner: Option<String>,
pub repo: Option<String>,
pub r#ref: Option<String>,

View File

@ -1,4 +1,6 @@
pub mod flake;
pub mod scan;
pub mod storage;
pub mod templates;
pub mod utils;
pub mod web;

83
src/scan.rs Normal file
View File

@ -0,0 +1,83 @@
use anyhow::{
Context,
Result,
};
use chrono::{
Utc,
};
use crate::{
flake::{
FlakeLocksNodeInputs,
FlakeMetadata,
FlakeUri,
},
storage::{
InputRow,
RevisionRow,
Storage,
},
};
use std::process::Command;
pub async fn scan_flake(storage: Storage, flake_uri: &str) -> Result<()> {
let scan_time = Utc::now().timestamp();
let flake_metadata_raw = Command::new("nix")
.arg("flake")
.arg("metadata")
.arg("--json")
.arg(flake_uri)
.output()
.context("Failed to fetch flake metadata")?;
let flake_metadata: FlakeMetadata = serde_json::from_slice(&flake_metadata_raw.stdout)
.context("Failed to parse flake metadata")?;
let revision_row = RevisionRow {
revision_uri: flake_metadata.locked.flake_uri()?.clone(),
flake_uri: Some(flake_metadata.resolved.flake_uri()?.clone()),
nix_store_path: Some(flake_metadata.path.clone()),
nar_hash: Some(flake_metadata.locked.nar_hash.clone()),
last_modified: Some(flake_metadata.locked.last_modified.clone()),
tracker_last_scanned: Some(scan_time.clone()),
};
storage.set_revision(revision_row)
.await?;
let locks_root_name = &flake_metadata.locks.root;
let locks_root_node = flake_metadata.locks.nodes.get(locks_root_name)
.context("Failed to get locks root node")?;
for (input_name, locks_input_name) in locks_root_node.inputs.clone().context("No inputs found for flake")? {
if let FlakeLocksNodeInputs::String(locks_input_name) = locks_input_name {
let locks_input_node = flake_metadata.locks.nodes.get(&locks_input_name)
.context("Failed to find lock of input")?;
let input_row = InputRow {
revision_uri: flake_metadata.locked.flake_uri()?.clone(),
input_name: input_name.clone(),
locked_revision_uri: Some(locks_input_node.locked.clone().context("Unexpected missing lock")?.flake_uri()?),
locked_flake_uri: Some(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?),
locked_nar_hash: Some(locks_input_node.locked.clone().context("Unexpected missing lock")?.nar_hash),
last_modified: Some(locks_input_node.locked.clone().context("Unexpected missing lock")?.last_modified),
};
storage.set_input(input_row)
.await?;
let revision_row = RevisionRow {
revision_uri: locks_input_node.locked.clone().context("Unexpected missing lock")?.flake_uri()?.clone(),
flake_uri: Some(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?.clone()),
nix_store_path: None,
nar_hash: None,
last_modified: None,
tracker_last_scanned: None,
};
storage.set_revision_exist(revision_row)
.await?;
}
}
Ok(())
}

129
src/web.rs Normal file
View File

@ -0,0 +1,129 @@
use anyhow::{
Context,
};
use askama::Template;
use axum::{
extract::{
Path,
State,
},
http::{
StatusCode,
},
response::{
IntoResponse,
Response,
},
Router,
routing::{
get,
},
};
use crate::{
storage::{
Storage,
},
templates::{
FlakeListTemplate,
FlakeTemplate,
IndexTemplate,
RevisionTemplate,
},
};
struct AppError(anyhow::Error);
impl std::fmt::Display for AppError {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, formatter)
}
}
impl std::fmt::Debug for AppError {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.0, formatter)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal server error:\n{:?}", self),
)
.into_response()
}
}
impl<E> From<E> for AppError where E: Into<anyhow::Error> {
fn from(err: E) -> Self {
Self(err.into())
}
}
fn render_template<T: Template>(t: &T) -> anyhow::Result<Response> {
let body = t.render()
.context("Failed to render template")?;
Ok((
[
("Content-Type", T::MIME_TYPE),
],
body,
).into_response())
}
#[derive(Clone)]
struct AppState {
storage: Storage,
}
pub fn make_router(storage: Storage) -> anyhow::Result<Router> {
let state = AppState {
storage,
};
let app = Router::new()
.route("/", get(route_index))
.route("/flakes", get(route_flakes))
.route("/f/{uri}", get(route_flake))
.route("/r/{revision_uri}", get(route_revision))
.with_state(state);
Ok(app)
}
async fn route_index() -> Result<impl IntoResponse, AppError> {
Ok(render_template(&IndexTemplate {})?)
}
async fn route_flakes(
State(state): State<AppState>,
) -> Result<impl IntoResponse, AppError> {
Ok(render_template(&FlakeListTemplate {
flakes: state.storage.flakes().await?,
})?)
}
async fn route_flake(
State(state): State<AppState>,
Path(uri): Path<String>,
) -> Result<impl IntoResponse, AppError> {
Ok(render_template(&FlakeTemplate {
uri: uri.clone(),
revisions: state.storage.revisions_from_flake(&uri).await?,
current_inputs: state.storage.current_inputs_for_flake(&uri).await?,
})?)
}
async fn route_revision(
State(state): State<AppState>,
Path(revision_uri): Path<String>,
) -> Result<impl IntoResponse, AppError> {
Ok(render_template(&RevisionTemplate {
revision_uri: revision_uri.clone(),
inputs: state.storage.inputs_for_revision(&revision_uri).await?,
input_of: state.storage.input_of_for_revision(&revision_uri).await?,
})?)
}