mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +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",
|
"hex",
|
||||||
"regex",
|
"regex",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha3",
|
"sha3",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
@ -126,6 +127,7 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ chrono.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
rusqlite.workspace = true
|
rusqlite.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
sha3.workspace = true
|
sha3.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub struct ArchivePaths {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||||
pub struct EntrySummary {
|
pub struct EntrySummary {
|
||||||
pub entry_uid: String,
|
pub entry_uid: String,
|
||||||
pub archived_at: String,
|
pub archived_at: String,
|
||||||
|
|
@ -27,7 +27,7 @@ pub struct EntrySummary {
|
||||||
pub total_artifact_bytes: i64,
|
pub total_artifact_bytes: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||||
pub struct EntryDetail {
|
pub struct EntryDetail {
|
||||||
pub summary: EntrySummary,
|
pub summary: EntrySummary,
|
||||||
pub structured_root_relpath: String,
|
pub structured_root_relpath: String,
|
||||||
|
|
@ -36,7 +36,7 @@ pub struct EntryDetail {
|
||||||
pub artifacts: Vec<EntryArtifactSummary>,
|
pub artifacts: Vec<EntryArtifactSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||||
pub struct EntryArtifactSummary {
|
pub struct EntryArtifactSummary {
|
||||||
pub artifact_role: String,
|
pub artifact_role: String,
|
||||||
pub storage_area: String,
|
pub storage_area: String,
|
||||||
|
|
@ -44,7 +44,7 @@ pub struct EntryArtifactSummary {
|
||||||
pub byte_size: Option<i64>,
|
pub byte_size: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||||
pub struct RunSummary {
|
pub struct RunSummary {
|
||||||
pub run_uid: String,
|
pub run_uid: String,
|
||||||
pub started_at: String,
|
pub started_at: String,
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ tower-http.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
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 {
|
use crate::registry::{MountedArchive, ServerRegistry};
|
||||||
Router::new().route("/health", get(|| async { "ok" }))
|
|
||||||
|
#[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