Compare commits
3 Commits
3adc3035f6
...
d2957d1799
Author | SHA1 | Date | |
---|---|---|---|
d2957d1799 | |||
f6733f86a7 | |||
7a4fbaf74a |
@ -2,25 +2,17 @@ use anyhow::{
|
|||||||
Context,
|
Context,
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
use chrono::{
|
|
||||||
Utc,
|
|
||||||
};
|
|
||||||
use clap::{
|
use clap::{
|
||||||
Parser,
|
Parser,
|
||||||
};
|
};
|
||||||
use flake_tracker::{
|
use flake_tracker::{
|
||||||
flake::{
|
scan::{
|
||||||
FlakeLocksNodeInputs,
|
scan_flake,
|
||||||
FlakeMetadata,
|
|
||||||
FlakeUri,
|
|
||||||
},
|
},
|
||||||
storage::{
|
storage::{
|
||||||
InputRow,
|
|
||||||
RevisionRow,
|
|
||||||
Storage,
|
Storage,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
@ -32,69 +24,11 @@ struct Cli {
|
|||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let scan_time = Utc::now().timestamp();
|
|
||||||
|
|
||||||
let storage = Storage::connect("sqlite://flake-tracker.db")
|
let storage = Storage::connect("sqlite://flake-tracker.db")
|
||||||
.await
|
.await
|
||||||
.context("Failed to connect to database")?;
|
.context("Failed to connect to database")?;
|
||||||
|
|
||||||
|
scan_flake(storage, &cli.flake_uri).await?;
|
||||||
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?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
116
src/bin/web.rs
116
src/bin/web.rs
@ -1,82 +1,15 @@
|
|||||||
use anyhow::{
|
use anyhow::{
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
use askama::Template;
|
|
||||||
use axum::{
|
|
||||||
extract::{
|
|
||||||
Path,
|
|
||||||
State,
|
|
||||||
},
|
|
||||||
http::{
|
|
||||||
StatusCode,
|
|
||||||
},
|
|
||||||
response::{
|
|
||||||
IntoResponse,
|
|
||||||
Response,
|
|
||||||
},
|
|
||||||
Router,
|
|
||||||
routing::{
|
|
||||||
get,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use flake_tracker::{
|
use flake_tracker::{
|
||||||
storage::{
|
storage::{
|
||||||
Storage,
|
Storage,
|
||||||
},
|
},
|
||||||
templates::{
|
web::{
|
||||||
FlakeListTemplate,
|
make_router,
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
@ -84,16 +17,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to connect to database")?;
|
.context("Failed to connect to database")?;
|
||||||
|
|
||||||
let state = AppState {
|
|
||||||
storage,
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = Router::new()
|
let app = make_router(storage)?;
|
||||||
.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 listener = tokio::net::TcpListener::bind("[::]:3000")
|
let listener = tokio::net::TcpListener::bind("[::]:3000")
|
||||||
.await
|
.await
|
||||||
@ -105,38 +30,3 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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?,
|
|
||||||
})?)
|
|
||||||
}
|
|
||||||
|
@ -10,7 +10,8 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct FlakeMetadata {
|
pub struct FlakeMetadata {
|
||||||
pub lastModified: i64,
|
#[serde(rename = "lastModified")]
|
||||||
|
pub last_modified: i64,
|
||||||
pub locked: FlakeLockedInfo,
|
pub locked: FlakeLockedInfo,
|
||||||
pub locks: FlakeLocks,
|
pub locks: FlakeLocks,
|
||||||
pub original: FlakeSource,
|
pub original: FlakeSource,
|
||||||
@ -100,8 +101,10 @@ impl FlakeUri for FlakeSource {
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct FlakeLockedInfo {
|
pub struct FlakeLockedInfo {
|
||||||
pub lastModified: i64,
|
#[serde(rename = "lastModified")]
|
||||||
pub narHash: String,
|
pub last_modified: i64,
|
||||||
|
#[serde(rename = "narHash")]
|
||||||
|
pub nar_hash: String,
|
||||||
pub owner: Option<String>,
|
pub owner: Option<String>,
|
||||||
pub repo: Option<String>,
|
pub repo: Option<String>,
|
||||||
pub r#ref: Option<String>,
|
pub r#ref: Option<String>,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
pub mod flake;
|
pub mod flake;
|
||||||
|
pub mod scan;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod templates;
|
pub mod templates;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod web;
|
||||||
|
83
src/scan.rs
Normal file
83
src/scan.rs
Normal 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
129
src/web.rs
Normal 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?,
|
||||||
|
})?)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user