use std::path::PathBuf;
use axum::{
body::Body,
extract::Query,
http::StatusCode,
response::{IntoResponse, Response},
};
use futures::TryStreamExt;
use serde::Deserialize;
use tokio_util::io::{ReaderStream, StreamReader, SyncIoBridge};
#[derive(Deserialize)]
pub struct PathQuery {
path: String,
}
pub async fn export(Query(q): Query<PathQuery>) -> Response {
let path = PathBuf::from(&q.path);
let name = match path.file_name() {
Some(n) => n.to_owned(),
None => return (StatusCode::BAD_REQUEST, "path has no file name").into_response(),
};
let metadata = match tokio::fs::metadata(&path).await {
Ok(m) => m,
Err(e) => {
return (StatusCode::NOT_FOUND, format!("stat {}: {e}", path.display()))
.into_response();
}
};
let is_dir = metadata.is_dir();
let (writer, reader) = tokio::io::duplex(64 * 1024);
let bridge = SyncIoBridge::new(writer);
tokio::task::spawn_blocking(move || {
let mut builder = tar::Builder::new(bridge);
let result = if is_dir {
builder.append_dir_all(&name, &path)
} else {
builder.append_path_with_name(&path, &name)
};
if let Err(e) = result.and_then(|_| builder.finish().map(|_| ())) {
tracing::warn!("laboratory export tar of {}: {e}", path.display());
}
});
Body::from_stream(ReaderStream::new(reader)).into_response()
}
pub async fn import(Query(q): Query<PathQuery>, body: Body) -> Response {
let dest = PathBuf::from(&q.path);
if let Err(e) = tokio::fs::create_dir_all(&dest).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("create_dir_all {}: {e}", dest.display()),
)
.into_response();
}
let stream = body.into_data_stream().map_err(std::io::Error::other);
let bridge = SyncIoBridge::new(StreamReader::new(stream));
let result = tokio::task::spawn_blocking(move || {
tar::Archive::new(bridge).unpack(&dest)
})
.await;
match result {
Ok(Ok(())) => StatusCode::NO_CONTENT.into_response(),
Ok(Err(e)) => (StatusCode::BAD_REQUEST, format!("unpack: {e}")).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("join: {e}")).into_response(),
}
}