use axum::Router;
use axum::extract::DefaultBodyLimit;
use axum::routing::{get, post};
use tower_http::limit::RequestBodyLimitLayer;
use crate::cnf::HTTP_MAX_ML_BODY_SIZE;
pub fn router<S>() -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
Router::new()
.route("/ml/import", post(implementation::import))
.route("/ml/export/{name}/{version}", get(implementation::export))
.route_layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new(*HTTP_MAX_ML_BODY_SIZE))
}
#[cfg(feature = "ml")]
mod implementation {
use anyhow::Context;
use axum::Extension;
use axum::body::Body;
use axum::extract::Path;
use axum::response::Response;
use bytes::Bytes;
use futures_util::StreamExt;
use http::StatusCode;
use surrealdb_core::dbs::Session;
use surrealdb_core::dbs::capabilities::RouteTarget;
use surrealdb_core::iam::check::check_ns_db;
use surrealdb_core::iam::{Action, ResourceKind};
use surrealdb_core::ml::storage::surml_file::SurMlFile;
use crate::ntw::AppState;
use crate::ntw::error::{Error as NetError, ResponseError};
use crate::ntw::output::Output;
pub async fn import(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
body: Body,
) -> Result<Output, ResponseError> {
let mut stream = body.into_data_stream();
let ds = &state.datastore;
if !ds.allows_http_route(&RouteTarget::Ml) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Ml);
return Err(NetError::ForbiddenRoute(RouteTarget::Ml.to_string()).into());
}
let (nsv, dbv) = check_ns_db(&session).map_err(ResponseError)?;
ds.check(&session, Action::Edit, ResourceKind::Model.on_db(&nsv, &dbv))
.map_err(ResponseError)?;
let mut buffer = Vec::new();
while let Some(chunk) = stream.next().await {
buffer.extend_from_slice(&chunk?);
}
let file =
SurMlFile::from_bytes(buffer).map_err(anyhow::Error::new).map_err(ResponseError)?;
if file.header.name.to_string() == "" || file.header.version.to_string() == "" {
return Err(ResponseError(anyhow::Error::msg("Model name and version must be set")));
}
let data = file.to_bytes();
ds.put_ml_model(
&session,
&file.header.name.to_string(),
&file.header.version.to_string(),
&file.header.description.to_string(),
data,
)
.await
.map_err(ResponseError)?;
Ok(Output::None)
}
pub async fn export(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
Path((name, version)): Path<(String, String)>,
) -> Result<Response, ResponseError> {
let ds = &state.datastore;
if !ds.allows_http_route(&RouteTarget::Ml) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Ml);
return Err(NetError::ForbiddenRoute(RouteTarget::Ml.to_string()).into());
}
let (nsv, dbv) = check_ns_db(&session).map_err(ResponseError)?;
ds.check(&session, Action::View, ResourceKind::Model.on_db(&nsv, &dbv))
.map_err(ResponseError)?;
let Some(info) =
ds.get_db_model(&nsv, &dbv, &name, &version).await.map_err(ResponseError)?
else {
return Err(NetError::NotFound(format!("Model {name} {version} not found")).into());
};
let path = format!("ml/{nsv}/{dbv}/{name}-{version}-{}.surml", info.hash);
let mut data = surrealdb_core::obs::stream(path)
.await
.context("Failed to read model file")
.map_err(ResponseError)?;
let (chn, body_stream) = surrealdb::channel::bounded::<Result<Bytes, anyhow::Error>>(1);
let body = Body::from_stream(body_stream);
tokio::spawn(async move {
while let Some(Ok(v)) = data.next().await {
let _ = chn.send(Ok(v)).await;
}
});
Ok(Response::builder().status(StatusCode::OK).body(body)?)
}
}
#[cfg(not(feature = "ml"))]
mod implementation {
use axum::Extension;
use axum::body::Body;
use axum::extract::Path;
use surrealdb_core::dbs::Session;
use surrealdb_core::dbs::capabilities::RouteTarget;
use crate::ntw::AppState;
use crate::ntw::error::{Error as NetError, ResponseError};
pub async fn import(
Extension(state): Extension<AppState>,
Extension(_): Extension<Session>,
_: Body,
) -> Result<(), ResponseError> {
let db = &state.datastore;
if !db.allows_http_route(&RouteTarget::Ml) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Ml);
return Err(NetError::ForbiddenRoute(RouteTarget::Ml.to_string()).into());
}
Err(NetError::Request.into())
}
pub async fn export(
Extension(state): Extension<AppState>,
Extension(_): Extension<Session>,
Path((_, _)): Path<(String, String)>,
) -> Result<(), ResponseError> {
let db = &state.datastore;
if !db.allows_http_route(&RouteTarget::Ml) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Ml);
return Err(NetError::ForbiddenRoute(RouteTarget::Ml.to_string()).into());
}
Err(NetError::Request.into())
}
}