#[macro_use] extern crate diesel; extern crate rayon; use actix_files::NamedFile; use actix_web::web::{HttpRequest, HttpResponse, Json}; use actix_web::{get, post, web, App, HttpServer, Responder}; use chrono::{Duration, Utc}; use data::{LoginRequest, ThumbnailRequest}; use jsonwebtoken::{encode, EncodingKey, Header}; use rayon::prelude::*; use serde::Serialize; use std::path::{Path, PathBuf}; use crate::data::{Claims, CreateAccountRequest, Token}; use crate::database::{create_user, get_user, user_exists}; use crate::files::list_files; use crate::video::create_playlist; mod data; mod database; mod files; mod video; #[post("/register")] async fn register(user: Json) -> impl Responder { if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation { if user_exists(&user.username) { HttpResponse::BadRequest() } else if let Some(_user) = create_user(&user.username, &user.password) { HttpResponse::Ok() } else { HttpResponse::InternalServerError() } } else { HttpResponse::BadRequest() } } #[post("/login")] async fn login(creds: Json) -> impl Responder { println!("Logging in: {}", creds.username); if let Some(user) = get_user(&creds.username, &creds.password) { let claims = Claims { sub: user.id.to_string(), exp: (Utc::now() + Duration::days(3)).timestamp(), }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret("secret_token".as_ref()), ) .unwrap(); HttpResponse::Ok().json(Token { token: &token }) } else { HttpResponse::NotFound().finish() } } #[post("/photos")] async fn list_photos(_claims: Claims, req: Json) -> impl Responder { println!("{}", req.path); let path = &req.path; if let Some(path) = is_valid_path(path) { let files = list_files(path).unwrap_or_default(); let photos = &files .iter() .filter(|f| !f.extension().unwrap_or_default().is_empty()) .map(|f| f.to_str().unwrap().to_string()) .collect::>(); let dirs = &files .iter() .filter(|f| f.extension().unwrap_or_default().is_empty()) .map(|f| f.to_str().unwrap().to_string()) .collect::>(); HttpResponse::Ok().json(PhotosResponse { photos, dirs }) } else { HttpResponse::BadRequest().finish() } } #[derive(Serialize)] struct PhotosResponse<'a> { photos: &'a [String], dirs: &'a [String], } fn is_valid_path(path: &str) -> Option { match path { path if path.contains("..") => None, path => { let path = PathBuf::from(path); if path.is_relative() { let mut full_path = PathBuf::from(dotenv::var("BASE_PATH").unwrap()); full_path.push(path); Some(full_path) } else { None } } } } #[get("/image")] async fn get_image( _claims: Claims, request: HttpRequest, req: web::Query, ) -> impl Responder { if let Some(path) = is_valid_path(&req.path) { if req.size.is_some() { let thumbs = dotenv::var("THUMBNAILS").unwrap(); let thumb_path = Path::new(&thumbs).join(path.file_name().unwrap()); println!("{:?}", thumb_path); if let Ok(file) = NamedFile::open(&thumb_path) { file.into_response(&request).unwrap() } else { HttpResponse::NotFound().finish() } } else if let Ok(file) = NamedFile::open(path) { file.into_response(&request).unwrap() } else { HttpResponse::NotFound().finish() } } else { HttpResponse::BadRequest().finish() } } #[post("/video/generate")] async fn generate_video(_claims: Claims, body: web::Json) -> impl Responder { let filename = PathBuf::from(&body.path); 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) { create_playlist(&path.to_str().unwrap(), &playlist); } else { return HttpResponse::BadRequest().finish(); } HttpResponse::Ok().json(playlist) } else { HttpResponse::BadRequest().finish() } } #[get("/video/stream")] async fn stream_video(request: HttpRequest, path: web::Query) -> impl Responder { let playlist = &path.path; println!("Playlist: {}", playlist); if let Ok(file) = NamedFile::open(playlist) { file.into_response(&request).unwrap() } else { HttpResponse::NotFound().finish() } } #[get("/video/{path}")] async fn get_video_part(request: HttpRequest, path: web::Path) -> impl Responder { let part = &path.path; println!("Video part: {}", part); if let Ok(file) = NamedFile::open(String::from("tmp/") + part) { file.into_response(&request).unwrap() } else { HttpResponse::NotFound().finish() } } async fn create_thumbnails() { let thumbs = &dotenv::var("THUMBNAILS").expect("THUMBNAILS not defined"); let thumbnail_directory: &Path = Path::new(thumbs); let t: Vec<_> = thumbnail_directory .read_dir() .expect("Error reading thumbnail directory") .collect(); if !t.is_empty() { println!("Skipping thumbs"); return; } let images = PathBuf::from(dotenv::var("BASE_PATH").unwrap()); walkdir::WalkDir::new(images) .into_iter() .collect::>>() .into_par_iter() .filter_map(|entry| entry.ok()) .filter(|entry| { println!("{:?}", entry.path()); if let Some(ext) = entry.path().extension().and_then(|ext| { ext.to_str().map(|ext| ext.to_lowercase()) }) { ext == "jpg" || ext == "jpeg" || ext == "png" } else { false } }) .map(|entry| (image::open(entry.path()), entry.path().to_path_buf())) .filter(|(img, _)| img.is_ok()) .map(|(img, path)| (img.unwrap(), path)) .map(|(image, path)| (image.thumbnail(200, 200), path)) .map(|(image, path)| { let thumb = Path::new(thumbnail_directory).join(path.file_name().unwrap()); println!("{:?}", thumb); image.save(thumb).expect("Failure saving thumbnail"); }) .for_each(drop); println!("Finished"); } #[actix_rt::main] async fn main() -> std::io::Result<()> { create_thumbnails().await; HttpServer::new(|| { App::new() .service(register) .service(login) .service(list_photos) .service(get_image) .service(generate_video) .service(stream_video) .service(get_video_part) }) .bind("192.168.10.23:8088")? .bind("localhost:8088")? .run() .await }