Improve testability and remove boxing
Some checks failed
Core Repos/ImageApi/pipeline/pr-master There was a failure building this commit

Leverage generics to remove the extra heap allocation for the response
handlers using Dao's. Also moved some of the environment variables to
app state to allow for easier testing.
This commit is contained in:
Cameron Cordes
2022-03-16 20:51:37 -04:00
parent e02165082a
commit 4d9b7c91a1
4 changed files with 172 additions and 124 deletions

View File

@@ -32,7 +32,7 @@ use log::{debug, error, info};
use crate::auth::login;
use crate::data::*;
use crate::database::*;
use crate::files::{is_image_or_video, is_valid_path};
use crate::files::{is_image_or_video, is_valid_full_path};
use crate::video::*;
mod auth;
@@ -62,13 +62,15 @@ async fn get_image(
_claims: Claims,
request: HttpRequest,
req: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
if let Some(path) = is_valid_path(&req.path) {
if let Some(path) = is_valid_full_path(&app_state.base_path, &req.path) {
if req.size.is_some() {
let thumbs = dotenv::var("THUMBNAILS").unwrap();
let relative_path = path
.strip_prefix(dotenv::var("BASE_PATH").unwrap())
.expect("Error stripping prefix");
.strip_prefix(&app_state.base_path)
.expect("Error stripping base path prefix from thumbnail");
let thumbs = &app_state.thumbnail_path;
let thumb_path = Path::new(&thumbs).join(relative_path);
debug!("{:?}", thumb_path);
@@ -89,8 +91,12 @@ async fn get_image(
}
#[get("/image/metadata")]
async fn get_file_metadata(_: Claims, path: web::Query<ThumbnailRequest>) -> impl Responder {
match is_valid_path(&path.path)
async fn get_file_metadata(
_: Claims,
path: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
match is_valid_full_path(&app_state.base_path, &path.path)
.ok_or_else(|| ErrorKind::InvalidData.into())
.and_then(File::open)
.and_then(|file| file.metadata())
@@ -107,7 +113,11 @@ async fn get_file_metadata(_: Claims, path: web::Query<ThumbnailRequest>) -> imp
}
#[post("/image")]
async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
async fn upload_image(
_: Claims,
mut payload: mp::Multipart,
app_state: web::Data<AppState>,
) -> impl Responder {
let mut file_content: BytesMut = BytesMut::new();
let mut file_name: Option<String> = None;
let mut file_path: Option<String> = None;
@@ -131,10 +141,12 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
}
}
let path = file_path.unwrap_or_else(|| dotenv::var("BASE_PATH").unwrap());
let path = file_path.unwrap_or_else(|| app_state.base_path.clone());
if !file_content.is_empty() {
let full_path = PathBuf::from(&path).join(file_name.unwrap());
if let Some(full_path) = is_valid_path(full_path.to_str().unwrap_or("")) {
if let Some(full_path) =
is_valid_full_path(&app_state.base_path, full_path.to_str().unwrap_or(""))
{
if !full_path.is_file() && is_image_or_video(&full_path) {
let mut file = File::create(full_path).unwrap();
file.write_all(&file_content).unwrap();
@@ -155,7 +167,7 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
#[post("/video/generate")]
async fn generate_video(
_claims: Claims,
data: web::Data<AppState>,
app_state: web::Data<AppState>,
body: web::Json<ThumbnailRequest>,
) -> impl Responder {
let filename = PathBuf::from(&body.path);
@@ -163,9 +175,10 @@ async fn generate_video(
if let Some(name) = filename.file_stem() {
let filename = name.to_str().expect("Filename should convert to string");
let playlist = format!("tmp/{}.m3u8", filename);
if let Some(path) = is_valid_path(&body.path) {
if let Some(path) = is_valid_full_path(&app_state.base_path, &body.path) {
if let Ok(child) = create_playlist(path.to_str().unwrap(), &playlist).await {
data.stream_manager
app_state
.stream_manager
.do_send(ProcessMessage(playlist.clone(), child));
}
} else {
@@ -184,12 +197,13 @@ async fn stream_video(
request: HttpRequest,
_: Claims,
path: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
let playlist = &path.path;
debug!("Playlist: {}", playlist);
// Extract video playlist dir to dotenv
if !playlist.starts_with("tmp") && is_valid_path(playlist) != None {
if !playlist.starts_with("tmp") && is_valid_full_path(&app_state.base_path, playlist) != None {
HttpResponse::BadRequest().finish()
} else if let Ok(file) = NamedFile::open(playlist) {
file.into_response(&request)
@@ -406,7 +420,56 @@ fn main() -> std::io::Result<()> {
env_logger::init();
create_thumbnails();
watch_files();
let system = actix::System::new();
system.block_on(async {
let app_data = web::Data::new(AppState::default());
let labels = HashMap::new();
let prometheus = PrometheusMetricsBuilder::new("api")
.const_labels(labels)
.build()
.expect("Unable to build prometheus metrics middleware");
prometheus
.registry
.register(Box::new(IMAGE_GAUGE.clone()))
.unwrap();
prometheus
.registry
.register(Box::new(VIDEO_GAUGE.clone()))
.unwrap();
HttpServer::new(move || {
let user_dao = SqliteUserDao::new();
let favorites_dao = SqliteFavoriteDao::new();
App::new()
.wrap(middleware::Logger::default())
.service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>)))
.service(web::resource("/photos").route(web::get().to(files::list_photos)))
.service(get_image)
.service(upload_image)
.service(generate_video)
.service(stream_video)
.service(get_video_part)
.service(favorites)
.service(put_add_favorite)
.service(delete_favorite)
.service(get_file_metadata)
.app_data(app_data.clone())
.app_data::<Data<SqliteUserDao>>(Data::new(user_dao))
.app_data::<Data<Box<dyn FavoriteDao>>>(Data::new(Box::new(favorites_dao)))
.wrap(prometheus.clone())
})
.bind(dotenv::var("BIND_URL").unwrap())?
.bind("localhost:8088")?
.run()
.await
})
}
fn watch_files() {
std::thread::spawn(|| {
let (wtx, wrx) = channel();
let mut watcher = watcher(wtx, std::time::Duration::from_secs(10)).unwrap();
@@ -449,58 +512,34 @@ fn main() -> std::io::Result<()> {
}
}
});
let system = actix::System::new();
system.block_on(async {
let act = StreamActor {}.start();
let app_data = web::Data::new(AppState {
stream_manager: Arc::new(act),
});
let labels = HashMap::new();
let prometheus = PrometheusMetricsBuilder::new("api")
.const_labels(labels)
.build()
.expect("Unable to build prometheus metrics middleware");
prometheus
.registry
.register(Box::new(IMAGE_GAUGE.clone()))
.unwrap();
prometheus
.registry
.register(Box::new(VIDEO_GAUGE.clone()))
.unwrap();
HttpServer::new(move || {
let user_dao = SqliteUserDao::new();
let favorites_dao = SqliteFavoriteDao::new();
App::new()
.wrap(middleware::Logger::default())
.service(web::resource("/login").route(web::post().to(login)))
.service(web::resource("/photos").route(web::get().to(files::list_photos)))
.service(get_image)
.service(upload_image)
.service(generate_video)
.service(stream_video)
.service(get_video_part)
.service(favorites)
.service(put_add_favorite)
.service(delete_favorite)
.service(get_file_metadata)
.app_data(app_data.clone())
.app_data::<Data<Box<dyn UserDao>>>(Data::new(Box::new(user_dao)))
.app_data::<Data<Box<dyn FavoriteDao>>>(Data::new(Box::new(favorites_dao)))
.wrap(prometheus.clone())
})
.bind(dotenv::var("BIND_URL").unwrap())?
.bind("localhost:8088")?
.run()
.await
})
}
struct AppState {
pub struct AppState {
stream_manager: Arc<Addr<StreamActor>>,
base_path: String,
thumbnail_path: String,
}
impl AppState {
fn new(
stream_manager: Arc<Addr<StreamActor>>,
base_path: String,
thumbnail_path: String,
) -> Self {
Self {
stream_manager,
base_path,
thumbnail_path,
}
}
}
impl Default for AppState {
fn default() -> Self {
Self::new(
Arc::new(StreamActor {}.start()),
env::var("BASE_PATH").expect("BASE_PATH was not set in the env"),
env::var("THUMBNAILS").expect("THUMBNAILS was not set in the env"),
)
}
}