site-backend/src/server/mod.rs

129 lines
3.6 KiB
Rust

mod matrix;
use axum::{
body::Body,
extract::{self, State},
response::Redirect,
routing::{any, post},
Router,
};
use flate2::read::GzDecoder;
use futures_util::TryStreamExt;
use tar::Archive;
use tokio::fs::File;
use tokio_util::io::StreamReader;
use tower_http::{
services::ServeDir, trace::TraceLayer, validate_request::ValidateRequestHeaderLayer,
};
use std::{
io,
path::{Path, PathBuf},
};
use crate::STATIC_ROOT_NAME;
pub fn app(
ctx: crate::Context,
api_key: &str,
redirects: &[(&'static str, &'static str)],
) -> Router {
// We first try to route the request according to the contents of the root directory. If the
// file doesn't exist, then we look for it in the other directories.
let serve_dir = ServeDir::new(ctx.static_dir.join(STATIC_ROOT_NAME))
.append_index_html_on_directories(true)
.not_found_service(
ServeDir::new(ctx.static_dir.clone()).append_index_html_on_directories(true),
);
let mut app = Router::new()
.route_service("/", serve_dir.clone())
.route(
"/{*path}",
post(post_static_archive)
.route_layer(ValidateRequestHeaderLayer::bearer(api_key))
.get_service(serve_dir),
)
.with_state(ctx.clone())
.merge(matrix::router());
for (path, url) in redirects.iter() {
app = app.route(path, any(|| async { Redirect::permanent(url) }))
}
app.layer(TraceLayer::new_for_http())
}
pub async fn post_static_archive(
State(ctx): State<crate::Context>,
extract::Path(path): extract::Path<String>,
body: Body,
) {
// Copy tarball data to file for parsing
let stream = body.into_data_stream();
let mut reader = StreamReader::new(stream.map_err(io::Error::other));
let uuid = uuid::Uuid::new_v4();
let ar_path = ctx.tmp_dir.join(uuid.to_string());
let mut f = File::create(&ar_path).await;
tokio::io::copy(&mut reader, &mut f.unwrap()).await;
// Root is stored in its own specifc directory, as otherwise it would wipe all other uploaded
// directories every time it's updated
let dest_dir = if path.is_empty() {
String::from(crate::STATIC_ROOT_NAME)
} else {
path
};
let dest_dir = ctx.static_dir.join(dest_dir);
tokio::task::spawn_blocking(move || process_archive(&ar_path, &dest_dir))
.await
.unwrap();
}
fn process_archive(ar_path: &Path, dest_dir: &Path) -> io::Result<()> {
let f = std::fs::File::open(ar_path)?;
let tar = GzDecoder::new(f);
let mut ar = Archive::new(tar);
// trim possible trailing slash from path
let dest_dir = PathBuf::from(dest_dir.to_string_lossy().trim_end_matches('/'));
// extract extension and append '.new' to form new extension
let ext = dest_dir
.extension()
.map(|ext| ext.to_string_lossy().to_string())
.unwrap_or(String::from(""));
let new_dir = dest_dir.with_extension(format!("{ext}.new"));
// Unpack archive into new directory
std::fs::create_dir(&new_dir)?;
ar.unpack(&new_dir)?;
// Replace original directory with new one
if dest_dir.try_exists()? {
std::fs::remove_dir_all(&dest_dir)?;
}
std::fs::rename(new_dir, dest_dir)?;
std::fs::remove_file(ar_path)?;
Ok(())
}
pub async fn delete_dir(
State(ctx): State<crate::Context>,
extract::Path(path): extract::Path<String>,
) {
let dest_dir = if path.is_empty() {
String::from(crate::STATIC_ROOT_NAME)
} else {
path
};
let dest_dir = ctx.static_dir.join(dest_dir);
tokio::fs::remove_dir_all(dest_dir).await;
}