mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat: expose archive server APIs
This commit is contained in:
parent
1634026e5b
commit
ed68a53c0b
5 changed files with 163 additions and 8 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -110,6 +110,7 @@ dependencies = [
|
|||
"hex",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha3",
|
||||
"uuid",
|
||||
|
|
@ -126,6 +127,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tower",
|
||||
"tower-http",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ chrono.workspace = true
|
|||
hex.workspace = true
|
||||
regex.workspace = true
|
||||
rusqlite.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha3.workspace = true
|
||||
uuid.workspace = true
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub struct ArchivePaths {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct EntrySummary {
|
||||
pub entry_uid: String,
|
||||
pub archived_at: String,
|
||||
|
|
@ -27,7 +27,7 @@ pub struct EntrySummary {
|
|||
pub total_artifact_bytes: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct EntryDetail {
|
||||
pub summary: EntrySummary,
|
||||
pub structured_root_relpath: String,
|
||||
|
|
@ -36,7 +36,7 @@ pub struct EntryDetail {
|
|||
pub artifacts: Vec<EntryArtifactSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct EntryArtifactSummary {
|
||||
pub artifact_role: String,
|
||||
pub storage_area: String,
|
||||
|
|
@ -44,7 +44,7 @@ pub struct EntryArtifactSummary {
|
|||
pub byte_size: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct RunSummary {
|
||||
pub run_uid: String,
|
||||
pub started_at: String,
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ tower-http.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
tower.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,158 @@
|
|||
use axum::{Router, routing::get};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::registry::ServerRegistry;
|
||||
use archivr_core::{archive, database};
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
|
||||
pub fn app(_registry: ServerRegistry) -> Router {
|
||||
Router::new().route("/health", get(|| async { "ok" }))
|
||||
use crate::registry::{MountedArchive, ServerRegistry};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
registry: Arc<ServerRegistry>,
|
||||
}
|
||||
|
||||
pub fn app(registry: ServerRegistry) -> Router {
|
||||
let state = AppState {
|
||||
registry: Arc::new(registry),
|
||||
};
|
||||
|
||||
Router::new()
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
.route("/api/archives", get(list_archives))
|
||||
.route("/api/archives/:archive_id/entries", get(list_entries))
|
||||
.route(
|
||||
"/api/archives/:archive_id/entries/:entry_uid",
|
||||
get(entry_detail),
|
||||
)
|
||||
.route("/api/archives/:archive_id/runs", get(list_runs))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn list_archives(State(state): State<AppState>) -> Json<Vec<MountedArchive>> {
|
||||
Json(state.registry.archives.clone())
|
||||
}
|
||||
|
||||
async fn list_entries(
|
||||
State(state): State<AppState>,
|
||||
Path(archive_id): Path<String>,
|
||||
) -> Result<Json<Vec<archive::EntrySummary>>, ApiError> {
|
||||
let mounted = mounted_archive(&state, &archive_id)?;
|
||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||
Ok(Json(archive::list_root_entries(&conn)?))
|
||||
}
|
||||
|
||||
async fn entry_detail(
|
||||
State(state): State<AppState>,
|
||||
Path((archive_id, entry_uid)): Path<(String, String)>,
|
||||
) -> Result<Json<archive::EntryDetail>, ApiError> {
|
||||
let mounted = mounted_archive(&state, &archive_id)?;
|
||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||
let detail = archive::get_entry_detail(&conn, &entry_uid)?
|
||||
.ok_or(ApiError::not_found("entry not found"))?;
|
||||
Ok(Json(detail))
|
||||
}
|
||||
|
||||
async fn list_runs(
|
||||
State(state): State<AppState>,
|
||||
Path(archive_id): Path<String>,
|
||||
) -> Result<Json<Vec<archive::RunSummary>>, ApiError> {
|
||||
let mounted = mounted_archive(&state, &archive_id)?;
|
||||
let conn = database::open_or_initialize(&mounted.archive_path)?;
|
||||
Ok(Json(archive::list_runs(&conn)?))
|
||||
}
|
||||
|
||||
fn mounted_archive<'a>(
|
||||
state: &'a AppState,
|
||||
archive_id: &str,
|
||||
) -> Result<&'a MountedArchive, ApiError> {
|
||||
state
|
||||
.registry
|
||||
.archives
|
||||
.iter()
|
||||
.find(|archive| archive.id == archive_id)
|
||||
.ok_or(ApiError::not_found("archive not found"))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiError {
|
||||
status: StatusCode,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
fn not_found(message: &str) -> Self {
|
||||
Self {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for ApiError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(error: E) -> Self {
|
||||
let error = error.into();
|
||||
Self {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("{error:#}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
(self.status, self.message).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn archives_endpoint_lists_mounted_archives() {
|
||||
let registry = ServerRegistry {
|
||||
archives: vec![MountedArchive {
|
||||
id: "personal".to_string(),
|
||||
label: "Personal".to_string(),
|
||||
archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"),
|
||||
}],
|
||||
};
|
||||
let response = app(registry)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/archives")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_archive_returns_404() {
|
||||
let response = app(ServerRegistry::default())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/archives/missing/entries")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue