From 7a3616186befb0bda40b39638b557d9b86ad5f94 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Thu, 22 Aug 2024 13:09:15 +0300 Subject: [PATCH] edit preview endpoint --- Cargo.lock | 14 +++ fl-server/Cargo.toml | 1 + fl-server/src/auth.rs | 2 +- fl-server/src/handlers.rs | 203 +++++++++++++++++++++++++++++++++- fl-server/src/main.rs | 4 + fl-server/src/response.rs | 8 +- fl-server/src/serve_flists.rs | 66 ----------- 7 files changed, 224 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a2ebd2..f1bf5a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,6 +1048,7 @@ dependencies = [ "rfs", "serde", "serde_json", + "sha256", "simple_logger", "tempdir", "thiserror", @@ -3080,6 +3081,19 @@ dependencies = [ "digest", ] +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" diff --git a/fl-server/Cargo.toml b/fl-server/Cargo.toml index 3475ad8..2111174 100644 --- a/fl-server/Cargo.toml +++ b/fl-server/Cargo.toml @@ -48,3 +48,4 @@ utoipa-swagger-ui = { version = "7", features = ["axum"] } thiserror = "1.0.63" hostname-validator = "1.1.1" walkdir = "2.5.0" +sha256 = "1.5.0" diff --git a/fl-server/src/auth.rs b/fl-server/src/auth.rs index 238e376..9cb96e6 100644 --- a/fl-server/src/auth.rs +++ b/fl-server/src/auth.rs @@ -78,7 +78,7 @@ pub async fn sign_in_handler( })) } -fn get_user_by_username(users: Vec, username: &str) -> Option { +pub fn get_user_by_username(users: Vec, username: &str) -> Option { let user = users.iter().find(|u| u.username == username)?; Some(user.clone()) } diff --git a/fl-server/src/handlers.rs b/fl-server/src/handlers.rs index 9b640c8..50474a7 100644 --- a/fl-server/src/handlers.rs +++ b/fl-server/src/handlers.rs @@ -1,3 +1,4 @@ +use anyhow::Error; use axum::{ extract::{Path, State}, response::IntoResponse, @@ -10,24 +11,31 @@ use std::{ sync::{mpsc, Arc}, }; use tokio::io; +use walkdir::WalkDir; use bollard::auth::DockerCredentials; use serde::{Deserialize, Serialize}; -use crate::auth::{SignInBody, SignInResponse, __path_sign_in_handler}; +use crate::{ + auth::{SignInBody, SignInResponse, __path_sign_in_handler, get_user_by_username, User}, + response::{DirListTemplate, DirLister, ErrorTemplate, TemplateErr}, +}; use crate::{ config::{self, Job}, response::{FileInfo, ResponseError, ResponseResult}, serve_flists::visit_dir_one_level, }; -use rfs::fungi::Writer; +use rfs::{ + cache, + fungi::{Reader, Writer}, +}; use utoipa::{OpenApi, ToSchema}; use uuid::Uuid; #[derive(OpenApi)] #[openapi( - paths(health_check_handler, create_flist_handler, get_flist_state_handler, list_flists_handler, sign_in_handler), - components(schemas(FlistBody, Job, ResponseError, ResponseResult, FileInfo, SignInBody, FlistState, SignInResponse, FlistStateInfo)), + paths(health_check_handler, create_flist_handler, get_flist_state_handler, preview_flist_handler, list_flists_handler, sign_in_handler), + components(schemas(DirListTemplate, DirLister, FlistBody, Job, ResponseError, ErrorTemplate, TemplateErr, ResponseResult, FileInfo, SignInBody, FlistState, SignInResponse, FlistStateInfo, PreviewResponse)), tags( (name = "fl-server", description = "Flist conversion API") ) @@ -48,6 +56,13 @@ pub struct FlistBody { pub registry_token: Option, } +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +pub struct PreviewResponse { + pub content: Vec, + pub metadata: String, + pub checksum: String, +} + #[derive(Debug, Clone, Serialize, PartialEq, ToSchema)] pub enum FlistState { Accepted(String), @@ -356,7 +371,57 @@ pub async fn list_flists_handler( Ok(ResponseResult::Flists(flists)) } -pub async fn flist_exists(dir_path: &std::path::Path, flist_name: &String) -> io::Result { +#[utoipa::path( + get, + path = "/v1/api/fl/preview/{flist_path}", + responses( + (status = 200, description = "Flist preview result", body = PreviewResponse), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized user"), + (status = 403, description = "Forbidden"), + (status = 500, description = "Internal server error"), + ), + params( + ("flist_path" = String, Path, description = "flist file path") + ) +)] +#[debug_handler] +pub async fn preview_flist_handler( + Extension(cfg): Extension, + Path(flist_path): Path, +) -> impl IntoResponse { + let fl_path = flist_path; + + match validate_flist_path(cfg.users, &cfg.flist_dir, &fl_path).await { + Ok(_) => (), + Err(err) => return Err(ResponseError::BadRequest(err.to_string())), + }; + + let content = match unpack_flist(&fl_path).await { + Ok(paths) => paths, + Err(_) => return Err(ResponseError::InternalServerError), + }; + + let bytes = match std::fs::read(&fl_path) { + Ok(b) => b, + Err(err) => { + log::error!( + "failed to read flist '{}' into bytes with error {}", + fl_path, + err + ); + return Err(ResponseError::InternalServerError); + } + }; + + Ok(ResponseResult::PreviewFlist(PreviewResponse { + content, + metadata: cfg.store_url.join("-"), + checksum: sha256::digest(&bytes), + })) +} + +async fn flist_exists(dir_path: &std::path::Path, flist_name: &String) -> io::Result { let mut dir = tokio::fs::read_dir(dir_path).await?; while let Some(child) = dir.next_entry().await? { @@ -369,3 +434,131 @@ pub async fn flist_exists(dir_path: &std::path::Path, flist_name: &String) -> io Ok(false) } + +async fn validate_flist_path( + users: Vec, + flist_dir: &String, + fl_path: &String, +) -> Result<(), Error> { + // validate path starting with `/` + if fl_path.starts_with("/") { + return Err(anyhow::anyhow!( + "invalid flist path '{}', shouldn't start with '/'", + fl_path + )); + } + + // path should include 3 parts [parent dir, username, flist file] + let parts: Vec<_> = fl_path.split("/").collect(); + if parts.len() != 3 { + return Err(anyhow::anyhow!( + format!("invalid flist path '{}', should consist of 3 parts [parent directory, username and flist name", fl_path + ))); + } + + // validate parent dir + if parts[0] != flist_dir { + return Err(anyhow::anyhow!( + "invalid flist path '{}', parent directory should be '{}'", + fl_path, + flist_dir + )); + } + + // validate username + match get_user_by_username(users, parts[1]) { + Some(_) => (), + None => { + return Err(anyhow::anyhow!( + "invalid flist path '{}', username '{}' doesn't exist", + fl_path, + parts[1] + )); + } + }; + + // validate flist extension + let fl_name = parts[2].to_string(); + let ext = match std::path::Path::new(&fl_name).extension() { + Some(ex) => ex.to_string_lossy().to_string(), + None => "".to_string(), + }; + + if ext != "fl" { + return Err(anyhow::anyhow!( + "invalid flist path '{}', invalid flist extension '{}' should be 'fl'", + fl_path, + ext + )); + } + + // validate flist existence + let username_dir = format!("{}/{}", parts[0], parts[1]); + match flist_exists(std::path::Path::new(&username_dir), &fl_name).await { + Ok(exists) => { + if !exists { + return Err(anyhow::anyhow!("flist '{}' doesn't exist", fl_path)); + } + } + Err(e) => { + log::error!("failed to check flist existence with error {:?}", e); + return Err(anyhow::anyhow!("Internal server error")); + } + } + + Ok(()) +} + +async fn unpack_flist(fl_path: &String) -> Result, Error> { + let meta = match Reader::new(&fl_path).await { + Ok(reader) => reader, + Err(err) => { + log::error!( + "failed to initialize metadata database for flist `{}` with error {}", + fl_path, + err + ); + return Err(anyhow::anyhow!("Internal server error")); + } + }; + + let router = match rfs::store::get_router(&meta).await { + Ok(r) => r, + Err(err) => { + log::error!("failed to get router with error {}", err); + return Err(anyhow::anyhow!("Internal server error")); + } + }; + + let cache = cache::Cache::new(String::from("/tmp/cache"), router); + let tmp_target = match tempdir::TempDir::new("target") { + Ok(dir) => dir, + Err(err) => { + log::error!("failed to create tmp dir with error {}", err); + return Err(anyhow::anyhow!("Internal server error")); + } + }; + let tmp_target_path = tmp_target.path().to_owned(); + + match rfs::unpack(&meta, &cache, &tmp_target_path, false).await { + Ok(_) => (), + Err(err) => { + log::error!("failed to unpack flist {} with error {}", fl_path, err); + return Err(anyhow::anyhow!("Internal server error")); + } + }; + + let mut paths = Vec::new(); + for file in WalkDir::new(tmp_target_path.clone()) + .into_iter() + .filter_map(|file| file.ok()) + { + let path = file.path().to_string_lossy().to_string(); + match path.strip_prefix(&tmp_target_path.to_string_lossy().to_string()) { + Some(p) => paths.push(p.to_string()), + None => return Err(anyhow::anyhow!("Internal server error")), + }; + } + + Ok(paths) +} diff --git a/fl-server/src/main.rs b/fl-server/src/main.rs index ecf3780..315e0d3 100644 --- a/fl-server/src/main.rs +++ b/fl-server/src/main.rs @@ -98,6 +98,10 @@ async fn app() -> Result<()> { auth::authorize, )), ) + .route( + "/v1/api/fl/preview/:flist_path", + get(handlers::preview_flist_handler), + ) .route("/v1/api/fl", get(handlers::list_flists_handler)) .route("/*path", get(serve_flists::serve_flists)); diff --git a/fl-server/src/response.rs b/fl-server/src/response.rs index 4f15379..124b1a0 100644 --- a/fl-server/src/response.rs +++ b/fl-server/src/response.rs @@ -10,7 +10,11 @@ use axum::{ use serde::Serialize; use utoipa::ToSchema; -use crate::{auth::SignInResponse, config::Job, handlers::FlistState}; +use crate::{ + auth::SignInResponse, + config::Job, + handlers::{FlistState, PreviewResponse}, +}; #[derive(Serialize, ToSchema)] pub enum ResponseError { @@ -75,7 +79,7 @@ pub enum ResponseResult { FlistCreated(Job), FlistState(FlistState), Flists(HashMap>), - PreviewFlist(Vec), + PreviewFlist(PreviewResponse), SignedIn(SignInResponse), DirTemplate(DirListTemplate), Res(hyper::Response), diff --git a/fl-server/src/serve_flists.rs b/fl-server/src/serve_flists.rs index dcf5b8f..48c08a8 100644 --- a/fl-server/src/serve_flists.rs +++ b/fl-server/src/serve_flists.rs @@ -3,7 +3,6 @@ use std::{path::PathBuf, sync::Arc}; use tokio::io; use tower::util::ServiceExt; use tower_http::services::ServeDir; -use walkdir::WalkDir; use axum::{ body::Body, @@ -12,7 +11,6 @@ use axum::{ }; use axum_macros::debug_handler; use percent_encoding::percent_decode; -use rfs::{cache, fungi::Reader}; use crate::{ config, @@ -29,13 +27,6 @@ pub async fn serve_flists( ) -> impl IntoResponse { let path = req.uri().path().to_string(); - if path.ends_with(".md") { - match preview_flist(&path).await { - Ok(res) => return Ok(res), - Err(err) => return Err(err), - }; - } - return match ServeDir::new("").oneshot(req).await { Ok(res) => { let status = res.status(); @@ -145,60 +136,3 @@ pub async fn visit_dir_one_level( Ok(files) } - -async fn preview_flist(path: &String) -> Result { - if !path.ends_with(".md") { - return Err(ResponseError::BadRequest( - "flist path is invalid".to_string(), - )); - } - - let mut fl_path: String = path.strip_suffix(".md").unwrap().to_string(); - fl_path = fl_path.strip_prefix("/").unwrap().to_string(); - let meta = match Reader::new(&fl_path).await { - Ok(reader) => reader, - Err(err) => { - log::error!( - "failed to initialize metadata database for flist `{}` with error {}", - fl_path, - err - ); - return Err(ResponseError::InternalServerError); - } - }; - - let router = match rfs::store::get_router(&meta).await { - Ok(r) => r, - Err(err) => { - log::error!("failed to get router with error {}", err); - return Err(ResponseError::InternalServerError); - } - }; - - let cache = cache::Cache::new(String::from("/tmp/cache"), router); - let tmp_target = tempdir::TempDir::new("target").unwrap(); - let tmp_target_path = tmp_target.path().to_owned(); - - match rfs::unpack(&meta, &cache, &tmp_target_path, false).await { - Ok(_) => (), - Err(err) => { - log::error!("failed to unpack flist {} with error {}", fl_path, err); - return Err(ResponseError::InternalServerError); - } - }; - - let mut paths = Vec::new(); - for file in WalkDir::new(tmp_target_path.clone()) - .into_iter() - .filter_map(|file| file.ok()) - { - let mut path = file.path().to_string_lossy().to_string(); - path = path - .strip_prefix(&tmp_target_path.to_string_lossy().to_string()) - .unwrap() - .to_string(); - paths.push(path); - } - - Ok(ResponseResult::PreviewFlist(paths)) -}