Merge branch 'master' into feature/tagging

This commit is contained in:
Cameron Cordes
2022-03-17 21:53:17 -04:00
10 changed files with 813 additions and 1114 deletions

1461
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,11 +10,11 @@ edition = "2018"
lto = true lto = true
[dependencies] [dependencies]
actix = "0.10" actix = "0.12"
actix-web = "3" actix-web = "4"
actix-rt = "1" actix-rt = "2.6"
actix-files = "0.5" actix-files = "0.6"
actix-multipart = "0.3.0" actix-multipart = "0.4.0"
futures = "0.3.5" futures = "0.3.5"
jsonwebtoken = "7.2.0" jsonwebtoken = "7.2.0"
serde = "1" serde = "1"
@@ -32,7 +32,7 @@ notify = "4.0"
path-absolutize = "3.0" path-absolutize = "3.0"
log="0.4" log="0.4"
env_logger="0.8" env_logger="0.8"
actix-web-prom = "0.5.1" actix-web-prom = "0.6"
prometheus = "0.11" prometheus = "0.13"
lazy_static = "1.1" lazy_static = "1.1"
anyhow = "1.0" anyhow = "1.0"

2
Jenkinsfile vendored
View File

@@ -1,7 +1,7 @@
pipeline { pipeline {
agent { agent {
docker { docker {
image 'rust:1.51' image 'rust:1.59'
args '-v "$PWD":/usr/src/image-api' args '-v "$PWD":/usr/src/image-api'
} }
} }

View File

@@ -1,5 +1,8 @@
use actix_web::web::{self, HttpResponse, Json}; use actix_web::Responder;
use actix_web::{post, Responder}; use actix_web::{
web::{self, Json},
HttpResponse,
};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use log::{debug, error}; use log::{debug, error};
@@ -9,10 +12,10 @@ use crate::{
database::UserDao, database::UserDao,
}; };
#[post("/register")] #[allow(dead_code)]
async fn register( async fn register<D: UserDao>(
user: Json<CreateAccountRequest>, user: Json<CreateAccountRequest>,
user_dao: web::Data<Box<dyn UserDao>>, user_dao: web::Data<D>,
) -> impl Responder { ) -> impl Responder {
if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation { if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation {
if user_dao.user_exists(&user.username) { if user_dao.user_exists(&user.username) {
@@ -27,10 +30,7 @@ async fn register(
} }
} }
pub async fn login( pub async fn login<D: UserDao>(creds: Json<LoginRequest>, user_dao: web::Data<D>) -> HttpResponse {
creds: Json<LoginRequest>,
user_dao: web::Data<Box<dyn UserDao>>,
) -> HttpResponse {
debug!("Logging in: {}", creds.username); debug!("Logging in: {}", creds.username);
if let Some(user) = user_dao.get_user(&creds.username, &creds.password) { if let Some(user) = user_dao.get_user(&creds.username, &creds.password) {
@@ -57,6 +57,7 @@ pub async fn login(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testhelpers::{BodyReader, TestUserDao}; use crate::testhelpers::{BodyReader, TestUserDao};
@@ -70,7 +71,7 @@ mod tests {
password: "pass".to_string(), password: "pass".to_string(),
}); });
let response = login(j, web::Data::new(Box::new(dao))).await; let response = login::<TestUserDao>(j, web::Data::new(dao)).await;
assert_eq!(response.status(), 200); assert_eq!(response.status(), 200);
} }
@@ -85,10 +86,12 @@ mod tests {
password: "password".to_string(), password: "password".to_string(),
}); });
let response = login(j, web::Data::new(Box::new(dao))).await; let response = login::<TestUserDao>(j, web::Data::new(dao)).await;
assert_eq!(response.status(), 200); assert_eq!(response.status(), 200);
assert!(response.body().read_to_str().contains("\"token\"")); let response_text: String = response.read_to_str();
assert!(response_text.contains("\"token\""));
} }
#[actix_rt::test] #[actix_rt::test]
@@ -101,7 +104,7 @@ mod tests {
password: "password".to_string(), password: "password".to_string(),
}); });
let response = login(j, web::Data::new(Box::new(dao))).await; let response = login::<TestUserDao>(j, web::Data::new(dao)).await;
assert_eq!(response.status(), 404); assert_eq!(response.status(), 404);
} }

View File

@@ -53,7 +53,6 @@ impl FromStr for Claims {
impl FromRequest for Claims { impl FromRequest for Claims {
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future { fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future {
req.headers() req.headers()

View File

@@ -1,3 +1,4 @@
use std::fmt::Debug;
use std::fs::read_dir; use std::fs::read_dir;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -5,17 +6,25 @@ use std::path::{Path, PathBuf};
use ::anyhow; use ::anyhow;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use actix_web::web::{HttpResponse, Query}; use actix_web::{
web::{self, Query},
HttpResponse,
};
use log::{debug, error}; use log::{debug, error};
use crate::data::{Claims, PhotosResponse, ThumbnailRequest}; use crate::data::{Claims, PhotosResponse, ThumbnailRequest};
use crate::AppState;
use path_absolutize::*; use path_absolutize::*;
pub async fn list_photos(_: Claims, req: Query<ThumbnailRequest>) -> HttpResponse { pub async fn list_photos(
_: Claims,
req: Query<ThumbnailRequest>,
app_state: web::Data<AppState>,
) -> HttpResponse {
let path = &req.path; let path = &req.path;
if let Some(path) = is_valid_path(path) { if let Some(path) = is_valid_full_path(&PathBuf::from(&app_state.base_path), path) {
debug!("Valid path: {:?}", path); debug!("Valid path: {:?}", path);
let files = list_files(&path).unwrap_or_default(); let files = list_files(&path).unwrap_or_default();
@@ -31,9 +40,7 @@ pub async fn list_photos(_: Claims, req: Query<ThumbnailRequest>) -> HttpRespons
) )
}) })
.map(|path: &PathBuf| { .map(|path: &PathBuf| {
let relative = path let relative = path.strip_prefix(&app_state.base_path).unwrap();
.strip_prefix(dotenv::var("BASE_PATH").unwrap())
.unwrap();
relative.to_path_buf() relative.to_path_buf()
}) })
.map(|f| f.to_str().unwrap().to_string()) .map(|f| f.to_str().unwrap().to_string())
@@ -43,9 +50,7 @@ pub async fn list_photos(_: Claims, req: Query<ThumbnailRequest>) -> HttpRespons
.iter() .iter()
.filter(|&f| f.metadata().map_or(false, |md| md.is_dir())) .filter(|&f| f.metadata().map_or(false, |md| md.is_dir()))
.map(|path: &PathBuf| { .map(|path: &PathBuf| {
let relative = path let relative = path.strip_prefix(&app_state.base_path).unwrap();
.strip_prefix(dotenv::var("BASE_PATH").unwrap())
.unwrap();
relative.to_path_buf() relative.to_path_buf()
}) })
.map(|f| f.to_str().unwrap().to_string()) .map(|f| f.to_str().unwrap().to_string())
@@ -82,18 +87,13 @@ pub fn is_image_or_video(path: &Path) -> bool {
|| extension == "nef" || extension == "nef"
} }
pub fn is_valid_path(path: &str) -> Option<PathBuf> { pub fn is_valid_full_path<P: AsRef<Path> + Debug>(base: &P, path: &str) -> Option<PathBuf> {
let base = PathBuf::from(dotenv::var("BASE_PATH").unwrap());
is_valid_full_path(&base, path)
}
fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
debug!("Base: {:?}. Path: {}", base, path); debug!("Base: {:?}. Path: {}", base, path);
let path = PathBuf::from(path); let path = PathBuf::from(path);
let mut path = if path.is_relative() { let mut path = if path.is_relative() {
let mut full_path = PathBuf::from(base); let mut full_path = PathBuf::new();
full_path.push(base);
full_path.push(&path); full_path.push(&path);
full_path full_path
} else { } else {
@@ -109,7 +109,10 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
} }
} }
fn is_path_above_base_dir(base: &Path, full_path: &mut PathBuf) -> anyhow::Result<PathBuf> { fn is_path_above_base_dir<P: AsRef<Path> + Debug>(
base: P,
full_path: &mut PathBuf,
) -> anyhow::Result<PathBuf> {
full_path full_path
.absolutize() .absolutize()
.with_context(|| format!("Unable to resolve absolute path: {:?}", full_path)) .with_context(|| format!("Unable to resolve absolute path: {:?}", full_path))
@@ -135,15 +138,21 @@ mod tests {
use super::*; use super::*;
mod api { mod api {
use actix_web::{web::Query, HttpResponse}; use super::*;
use actix::Actor;
use super::list_photos; use actix_web::{
use crate::{ web::{self, Query},
data::{Claims, PhotosResponse, ThumbnailRequest}, HttpResponse,
testhelpers::TypedBodyReader,
}; };
use std::fs; use crate::{
data::{Claims, PhotosResponse, ThumbnailRequest},
testhelpers::BodyReader,
video::StreamActor,
AppState,
};
use std::{fs, sync::Arc};
fn setup() { fn setup() {
let _ = env_logger::builder().is_test(true).try_init(); let _ = env_logger::builder().is_test(true).try_init();
@@ -160,7 +169,6 @@ mod tests {
let request: Query<ThumbnailRequest> = Query::from_query("path=").unwrap(); let request: Query<ThumbnailRequest> = Query::from_query("path=").unwrap();
std::env::set_var("BASE_PATH", "/tmp");
let mut temp_photo = std::env::temp_dir(); let mut temp_photo = std::env::temp_dir();
let mut tmp = temp_photo.clone(); let mut tmp = temp_photo.clone();
@@ -169,13 +177,23 @@ mod tests {
temp_photo.push("photo.jpg"); temp_photo.push("photo.jpg");
fs::File::create(temp_photo).unwrap(); fs::File::create(temp_photo.clone()).unwrap();
let response: HttpResponse = list_photos(claims, request).await; let response: HttpResponse = list_photos(
claims,
request,
web::Data::new(AppState::new(
Arc::new(StreamActor {}.start()),
String::from("/tmp"),
String::from("/tmp/thumbs"),
)),
)
.await;
let status = response.status();
let body: PhotosResponse = response.body().read_body(); let body: PhotosResponse = serde_json::from_str(&response.read_to_str()).unwrap();
assert_eq!(response.status(), 200); assert_eq!(status, 200);
assert!(body.photos.contains(&String::from("photo.jpg"))); assert!(body.photos.contains(&String::from("photo.jpg")));
assert!(body.dirs.contains(&String::from("test-dir"))); assert!(body.dirs.contains(&String::from("test-dir")));
assert!(body assert!(body
@@ -199,7 +217,16 @@ mod tests {
let request: Query<ThumbnailRequest> = Query::from_query("path=..").unwrap(); let request: Query<ThumbnailRequest> = Query::from_query("path=..").unwrap();
let response = list_photos(claims, request).await; let response = list_photos(
claims,
request,
web::Data::new(AppState::new(
Arc::new(StreamActor {}.start()),
String::from("/tmp"),
String::from("/tmp/thumbs"),
)),
)
.await;
assert_eq!(response.status(), 400); assert_eq!(response.status(), 400);
} }
@@ -207,12 +234,13 @@ mod tests {
#[test] #[test]
fn directory_traversal_test() { fn directory_traversal_test() {
assert_eq!(None, is_valid_path("../")); let base = env::temp_dir();
assert_eq!(None, is_valid_path("..")); assert_eq!(None, is_valid_full_path(&base, "../"));
assert_eq!(None, is_valid_path("fake/../../../")); assert_eq!(None, is_valid_full_path(&base, ".."));
assert_eq!(None, is_valid_path("../../../etc/passwd")); assert_eq!(None, is_valid_full_path(&base, "fake/../../../"));
assert_eq!(None, is_valid_path("..//etc/passwd")); assert_eq!(None, is_valid_full_path(&base, "../../../etc/passwd"));
assert_eq!(None, is_valid_path("../../etc/passwd")); assert_eq!(None, is_valid_full_path(&base, "..//etc/passwd"));
assert_eq!(None, is_valid_full_path(&base, "../../etc/passwd"));
} }
#[test] #[test]

View File

@@ -2,12 +2,13 @@
extern crate diesel; extern crate diesel;
extern crate rayon; extern crate rayon;
use actix_web_prom::PrometheusMetrics; use actix_web::web::Data;
use actix_web_prom::PrometheusMetricsBuilder;
use chrono::Utc; use chrono::Utc;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use prometheus::{self, IntGauge}; use prometheus::{self, IntGauge};
use std::sync::{mpsc::channel, Arc}; use std::sync::mpsc::channel;
use std::{collections::HashMap, io::prelude::*}; use std::{collections::HashMap, io::prelude::*};
use std::{env, fs::File}; use std::{env, fs::File};
use std::{ use std::{
@@ -16,15 +17,12 @@ use std::{
}; };
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use actix::prelude::*;
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_multipart as mp; use actix_multipart as mp;
use actix_web::{ use actix_web::{
delete, delete, get, middleware, post, put,
error::BlockingError, web::{self, BufMut, BytesMut},
get, middleware, post, put, App, HttpRequest, HttpResponse, HttpServer, Responder,
web::{self, BufMut, BytesMut, HttpRequest, HttpResponse},
App, HttpServer, Responder,
}; };
use diesel::prelude::*; use diesel::prelude::*;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
@@ -35,14 +33,16 @@ use log::{debug, error, info};
use crate::auth::login; use crate::auth::login;
use crate::data::*; use crate::data::*;
use crate::database::*; 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::models::{InsertTag, InsertTaggedPhoto, Tag, TaggedPhoto}; use crate::models::{InsertTag, InsertTaggedPhoto, Tag, TaggedPhoto};
use crate::state::AppState;
use crate::video::*; use crate::video::*;
mod auth; mod auth;
mod data; mod data;
pub mod database; pub mod database;
mod files; mod files;
mod state;
mod video; mod video;
#[cfg(test)] #[cfg(test)]
@@ -66,23 +66,25 @@ async fn get_image(
_claims: Claims, _claims: Claims,
request: HttpRequest, request: HttpRequest,
req: web::Query<ThumbnailRequest>, req: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>,
) -> impl Responder { ) -> 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() { if req.size.is_some() {
let thumbs = dotenv::var("THUMBNAILS").unwrap();
let relative_path = path let relative_path = path
.strip_prefix(dotenv::var("BASE_PATH").unwrap()) .strip_prefix(&app_state.base_path)
.expect("Error stripping prefix"); .expect("Error stripping base path prefix from thumbnail");
let thumbs = &app_state.thumbnail_path;
let thumb_path = Path::new(&thumbs).join(relative_path); let thumb_path = Path::new(&thumbs).join(relative_path);
debug!("{:?}", thumb_path); debug!("{:?}", thumb_path);
if let Ok(file) = NamedFile::open(&thumb_path) { if let Ok(file) = NamedFile::open(&thumb_path) {
file.into_response(&request).unwrap() file.into_response(&request)
} else { } else {
HttpResponse::NotFound().finish() HttpResponse::NotFound().finish()
} }
} else if let Ok(file) = NamedFile::open(path) { } else if let Ok(file) = NamedFile::open(path) {
file.into_response(&request).unwrap() file.into_response(&request)
} else { } else {
HttpResponse::NotFound().finish() HttpResponse::NotFound().finish()
} }
@@ -93,8 +95,12 @@ async fn get_image(
} }
#[get("/image/metadata")] #[get("/image/metadata")]
async fn get_file_metadata(_: Claims, path: web::Query<ThumbnailRequest>) -> impl Responder { async fn get_file_metadata(
match is_valid_path(&path.path) _: 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()) .ok_or_else(|| ErrorKind::InvalidData.into())
.and_then(File::open) .and_then(File::open)
.and_then(|file| file.metadata()) .and_then(|file| file.metadata())
@@ -111,13 +117,17 @@ async fn get_file_metadata(_: Claims, path: web::Query<ThumbnailRequest>) -> imp
} }
#[post("/image")] #[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_content: BytesMut = BytesMut::new();
let mut file_name: Option<String> = None; let mut file_name: Option<String> = None;
let mut file_path: Option<String> = None; let mut file_path: Option<String> = None;
while let Some(Ok(mut part)) = payload.next().await { while let Some(Ok(mut part)) = payload.next().await {
if let Some(content_type) = part.content_disposition() { let content_type = part.content_disposition();
debug!("{:?}", content_type); debug!("{:?}", content_type);
if let Some(filename) = content_type.get_filename() { if let Some(filename) = content_type.get_filename() {
debug!("Name: {:?}", filename); debug!("Name: {:?}", filename);
@@ -134,12 +144,13 @@ 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() { if !file_content.is_empty() {
let full_path = PathBuf::from(&path).join(file_name.unwrap()); 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) { if !full_path.is_file() && is_image_or_video(&full_path) {
let mut file = File::create(full_path).unwrap(); let mut file = File::create(full_path).unwrap();
file.write_all(&file_content).unwrap(); file.write_all(&file_content).unwrap();
@@ -160,7 +171,7 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
#[post("/video/generate")] #[post("/video/generate")]
async fn generate_video( async fn generate_video(
_claims: Claims, _claims: Claims,
data: web::Data<AppState>, app_state: web::Data<AppState>,
body: web::Json<ThumbnailRequest>, body: web::Json<ThumbnailRequest>,
) -> impl Responder { ) -> impl Responder {
let filename = PathBuf::from(&body.path); let filename = PathBuf::from(&body.path);
@@ -168,9 +179,10 @@ async fn generate_video(
if let Some(name) = filename.file_stem() { if let Some(name) = filename.file_stem() {
let filename = name.to_str().expect("Filename should convert to string"); let filename = name.to_str().expect("Filename should convert to string");
let playlist = format!("tmp/{}.m3u8", filename); 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 { 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)); .do_send(ProcessMessage(playlist.clone(), child));
} }
} else { } else {
@@ -189,15 +201,16 @@ async fn stream_video(
request: HttpRequest, request: HttpRequest,
_: Claims, _: Claims,
path: web::Query<ThumbnailRequest>, path: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>,
) -> impl Responder { ) -> impl Responder {
let playlist = &path.path; let playlist = &path.path;
debug!("Playlist: {}", playlist); debug!("Playlist: {}", playlist);
// Extract video playlist dir to dotenv // 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() HttpResponse::BadRequest().finish()
} else if let Ok(file) = NamedFile::open(playlist) { } else if let Ok(file) = NamedFile::open(playlist) {
file.into_response(&request).unwrap() file.into_response(&request)
} else { } else {
HttpResponse::NotFound().finish() HttpResponse::NotFound().finish()
} }
@@ -213,7 +226,7 @@ async fn get_video_part(
debug!("Video part: {}", part); debug!("Video part: {}", part);
if let Ok(file) = NamedFile::open(String::from("tmp/") + part) { if let Ok(file) = NamedFile::open(String::from("tmp/") + part) {
file.into_response(&request).unwrap() file.into_response(&request)
} else { } else {
error!("Video part not found: tmp/{}", part); error!("Video part not found: tmp/{}", part);
HttpResponse::NotFound().finish() HttpResponse::NotFound().finish()
@@ -225,10 +238,10 @@ async fn favorites(
claims: Claims, claims: Claims,
favorites_dao: web::Data<Box<dyn FavoriteDao>>, favorites_dao: web::Data<Box<dyn FavoriteDao>>,
) -> impl Responder { ) -> impl Responder {
let favorites = match web::block(move || favorites_dao.get_favorites(claims.sub.parse::<i32>().unwrap())).await
web::block(move || favorites_dao.get_favorites(claims.sub.parse::<i32>().unwrap())) {
.await Ok(Ok(favorites)) => {
.unwrap() let favorites = favorites
.into_iter() .into_iter()
.map(|favorite| favorite.path) .map(|favorite| favorite.path)
.collect::<Vec<String>>(); .collect::<Vec<String>>();
@@ -237,6 +250,13 @@ async fn favorites(
photos: favorites, photos: favorites,
dirs: Vec::new(), dirs: Vec::new(),
}) })
}
Ok(Err(e)) => {
error!("Error getting favorites: {:?}", e);
HttpResponse::InternalServerError().finish()
}
Err(_) => HttpResponse::InternalServerError().finish(),
}
} }
#[put("image/favorites")] #[put("image/favorites")]
@@ -247,21 +267,27 @@ async fn put_add_favorite(
) -> impl Responder { ) -> impl Responder {
if let Ok(user_id) = claims.sub.parse::<i32>() { if let Ok(user_id) = claims.sub.parse::<i32>() {
let path = body.path.clone(); let path = body.path.clone();
match web::block::<_, usize, DbError>(move || favorites_dao.add_favorite(user_id, &path)) match web::block::<_, Result<usize, DbError>>(move || {
favorites_dao.add_favorite(user_id, &path)
})
.await .await
{ {
Err(BlockingError::Error(e)) if e.kind == DbErrorKind::AlreadyExists => { Ok(Err(e)) if e.kind == DbErrorKind::AlreadyExists => {
debug!("Favorite: {} exists for user: {}", &body.path, user_id); debug!("Favorite: {} exists for user: {}", &body.path, user_id);
HttpResponse::Ok() HttpResponse::Ok()
} }
Err(e) => { Ok(Err(e)) => {
error!("{:?} {}. for user: {}", e, body.path, user_id); info!("{:?} {}. for user: {}", e, body.path, user_id);
HttpResponse::BadRequest() HttpResponse::BadRequest()
} }
Ok(_) => { Ok(Ok(_)) => {
info!("Adding favorite \"{}\" for userid: {}", body.path, user_id); debug!("Adding favorite \"{}\" for userid: {}", body.path, user_id);
HttpResponse::Created() HttpResponse::Created()
} }
Err(e) => {
error!("Blocking error while inserting favorite: {:?}", e);
HttpResponse::InternalServerError()
}
} }
} else { } else {
error!("Unable to parse sub as i32: {}", claims.sub); error!("Unable to parse sub as i32: {}", claims.sub);
@@ -277,9 +303,8 @@ async fn delete_favorite(
) -> impl Responder { ) -> impl Responder {
if let Ok(user_id) = claims.sub.parse::<i32>() { if let Ok(user_id) = claims.sub.parse::<i32>() {
let path = body.path.clone(); let path = body.path.clone();
web::block::<_, _, String>(move || { web::block(move || {
favorites_dao.remove_favorite(user_id, path); favorites_dao.remove_favorite(user_id, path);
Ok(())
}) })
.await .await
.unwrap(); .unwrap();
@@ -538,7 +563,59 @@ fn main() -> std::io::Result<()> {
env_logger::init(); env_logger::init();
create_thumbnails(); 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)
.service(add_tag)
.service(get_tags)
.service(remove_tagged_photo)
.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(|| { std::thread::spawn(|| {
let (wtx, wrx) = channel(); let (wtx, wrx) = channel();
let mut watcher = watcher(wtx, std::time::Duration::from_secs(10)).unwrap(); let mut watcher = watcher(wtx, std::time::Duration::from_secs(10)).unwrap();
@@ -581,56 +658,4 @@ fn main() -> std::io::Result<()> {
} }
} }
}); });
let system = actix::System::new("image-api");
let act = StreamActor {}.start();
let app_data = web::Data::new(AppState {
stream_manager: Arc::new(act),
});
let labels = HashMap::new();
let prometheus = PrometheusMetrics::new("", Some("/metrics"), Some(labels));
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)
.service(add_tag)
.service(get_tags)
.service(remove_tagged_photo)
.app_data(app_data.clone())
.data::<Box<dyn UserDao>>(Box::new(user_dao))
.data::<Box<dyn FavoriteDao>>(Box::new(favorites_dao))
.wrap(prometheus.clone())
})
.bind(dotenv::var("BIND_URL").unwrap())?
.bind("localhost:8088")?
.run();
system.run()
}
struct AppState {
stream_manager: Arc<Addr<StreamActor>>,
} }

33
src/state.rs Normal file
View File

@@ -0,0 +1,33 @@
use crate::StreamActor;
use actix::{Actor, Addr};
use std::{env, sync::Arc};
pub struct AppState {
pub stream_manager: Arc<Addr<StreamActor>>,
pub base_path: String,
pub thumbnail_path: String,
}
impl AppState {
pub 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"),
)
}
}

View File

@@ -1,5 +1,7 @@
use actix_web::dev::{Body, ResponseBody}; use actix_web::{
use serde::Deserialize; body::{BoxBody, MessageBody},
HttpResponse,
};
use crate::database::{models::User, UserDao}; use crate::database::{models::User, UserDao};
use std::cell::RefCell; use std::cell::RefCell;
@@ -55,32 +57,12 @@ impl UserDao for TestUserDao {
} }
pub trait BodyReader { pub trait BodyReader {
fn read_to_str(&self) -> &str; fn read_to_str(self) -> String;
} }
impl BodyReader for ResponseBody<Body> { impl BodyReader for HttpResponse<BoxBody> {
fn read_to_str(&self) -> &str { fn read_to_str(self) -> String {
match self { let body = self.into_body().try_into_bytes().unwrap();
ResponseBody::Body(Body::Bytes(ref b)) => std::str::from_utf8(b).unwrap(), std::str::from_utf8(&body).unwrap().to_string()
_ => panic!("Unknown response body"),
}
}
}
pub trait TypedBodyReader<'a, T>
where
T: Deserialize<'a>,
{
fn read_body(&'a self) -> T;
}
impl<'a, T: Deserialize<'a>> TypedBodyReader<'a, T> for ResponseBody<Body> {
fn read_body(&'a self) -> T {
match self {
ResponseBody::Body(Body::Bytes(ref b)) => {
serde_json::from_str(std::str::from_utf8(b).unwrap()).unwrap()
}
_ => panic!("Unknown response body"),
}
} }
} }

View File

@@ -67,7 +67,7 @@ pub async fn create_playlist(video_path: &str, playlist_file: &str) -> Result<Ch
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
loop { loop {
actix::clock::delay_for(std::time::Duration::from_secs(1)).await; actix::clock::sleep(std::time::Duration::from_secs(1)).await;
if Path::new(playlist_file).exists() if Path::new(playlist_file).exists()
|| std::time::Instant::now() - start_time > std::time::Duration::from_secs(5) || std::time::Instant::now() - start_time > std::time::Duration::from_secs(5)