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, extract::Path(path): extract::Path, 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, extract::Path(path): extract::Path, ) { 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; }