feature/tagging #16
1461
Cargo.lock
generated
1461
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -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
2
Jenkinsfile
vendored
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/auth.rs
29
src/auth.rs
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
102
src/files.rs
102
src/files.rs
@@ -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]
|
||||||
|
|||||||
209
src/main.rs
209
src/main.rs
@@ -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
33
src/state.rs
Normal 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"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user