Merge pull request 'Image Upload' (#1) from feature/image-upload into master
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
1028
Cargo.lock
generated
1028
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,10 @@ edition = "2018"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "2.0"
|
actix-web = "3.0"
|
||||||
actix-rt = "1.0"
|
actix-rt = "1.0"
|
||||||
actix-files = "0.2.2"
|
actix-files = "0.3.0"
|
||||||
|
actix-multipart = "0.3.0"
|
||||||
futures = "0.3.5"
|
futures = "0.3.5"
|
||||||
jsonwebtoken = "7.2.0"
|
jsonwebtoken = "7.2.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
@@ -23,3 +24,6 @@ bcrypt = "0.8.1"
|
|||||||
image = "0.23.7"
|
image = "0.23.7"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
rayon = "1.3"
|
rayon = "1.3"
|
||||||
|
notify = "4.0"
|
||||||
|
tokio = "0.2"
|
||||||
|
path-absolutize = "3.0.6"
|
||||||
|
|||||||
42
src/files.rs
42
src/files.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use path_absolutize::*;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::read_dir;
|
use std::fs::read_dir;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -32,6 +33,7 @@ fn is_image_or_video(path: &Path) -> bool {
|
|||||||
|| extension == "jpeg"
|
|| extension == "jpeg"
|
||||||
|| extension == "mp4"
|
|| extension == "mp4"
|
||||||
|| extension == "mov"
|
|| extension == "mov"
|
||||||
|
|| extension == "nef"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_valid_path(path: &str) -> Option<PathBuf> {
|
pub fn is_valid_path(path: &str) -> Option<PathBuf> {
|
||||||
@@ -46,10 +48,10 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
|
|||||||
let mut full_path = PathBuf::from(base);
|
let mut full_path = PathBuf::from(base);
|
||||||
full_path.push(&path);
|
full_path.push(&path);
|
||||||
full_path
|
full_path
|
||||||
.canonicalize()
|
.absolutize()
|
||||||
.and_then(|p| {
|
.and_then(|p| {
|
||||||
if p.starts_with(base) {
|
if p.starts_with(base) {
|
||||||
Ok(p)
|
Ok(p.into_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(io::Error::new(
|
Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
@@ -58,6 +60,17 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
|
} else if let Ok(path) = path.absolutize().and_then(|path| {
|
||||||
|
if path.starts_with(base) {
|
||||||
|
Ok(path.into_owned())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Path below base directory",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Some(path)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -76,9 +89,7 @@ mod tests {
|
|||||||
assert_eq!(None, is_valid_path("fake/../../../"));
|
assert_eq!(None, is_valid_path("fake/../../../"));
|
||||||
assert_eq!(None, is_valid_path("../../../etc/passwd"));
|
assert_eq!(None, is_valid_path("../../../etc/passwd"));
|
||||||
assert_eq!(None, is_valid_path("..//etc/passwd"));
|
assert_eq!(None, is_valid_path("..//etc/passwd"));
|
||||||
assert_eq!(None, is_valid_path("..%2f..%2f/etc/passwd"));
|
assert_eq!(None, is_valid_path("../../etc/passwd"));
|
||||||
assert_eq!(None, is_valid_path("%2e%2e%2f%2e%2e%2f/etc/passwd"));
|
|
||||||
assert_eq!(None, is_valid_path("\\\\attacker.com\\shared\\mal.php"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -102,6 +113,27 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_from_absolute_path_test() {
|
||||||
|
let base = env::temp_dir();
|
||||||
|
let mut test_file = PathBuf::from(&base);
|
||||||
|
test_file.push("test.png");
|
||||||
|
File::create(&test_file).unwrap();
|
||||||
|
|
||||||
|
assert!(is_valid_full_path(&base, test_file.to_str().unwrap()).is_some());
|
||||||
|
|
||||||
|
let path = "relative/path/test.png";
|
||||||
|
let mut test_file = PathBuf::from(&base);
|
||||||
|
test_file.push(path);
|
||||||
|
create_dir_all(test_file.parent().unwrap()).unwrap();
|
||||||
|
File::create(test_file).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(PathBuf::from("/tmp/relative/path/test.png")),
|
||||||
|
is_valid_full_path(&base, path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn png_valid_extension_test() {
|
fn png_valid_extension_test() {
|
||||||
assert!(is_image_or_video(Path::new("image.png")));
|
assert!(is_image_or_video(Path::new("image.png")));
|
||||||
|
|||||||
84
src/main.rs
84
src/main.rs
@@ -3,14 +3,20 @@ extern crate diesel;
|
|||||||
extern crate rayon;
|
extern crate rayon;
|
||||||
|
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
|
use actix_multipart as mp;
|
||||||
use actix_web::web::{HttpRequest, HttpResponse, Json};
|
use actix_web::web::{HttpRequest, HttpResponse, Json};
|
||||||
use actix_web::{get, post, web, App, HttpServer, Responder};
|
use actix_web::{get, post, web, App, HttpServer, Responder};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use data::{AddFavoriteRequest, LoginRequest, ThumbnailRequest};
|
use data::{AddFavoriteRequest, LoginRequest, ThumbnailRequest};
|
||||||
|
use futures::stream::StreamExt;
|
||||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
|
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
use crate::data::{secret_key, Claims, CreateAccountRequest, Token};
|
use crate::data::{secret_key, Claims, CreateAccountRequest, Token};
|
||||||
use crate::database::{add_favorite, create_user, get_favorites, get_user, user_exists};
|
use crate::database::{add_favorite, create_user, get_favorites, get_user, user_exists};
|
||||||
@@ -119,6 +125,51 @@ async fn get_image(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/image")]
|
||||||
|
async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
|
||||||
|
let mut file_content: Vec<_> = Vec::new();
|
||||||
|
let mut file_name: Option<String> = None;
|
||||||
|
let mut file_path: Option<String> = None;
|
||||||
|
|
||||||
|
while let Some(Ok(mut part)) = payload.next().await {
|
||||||
|
if let Some(content_type) = part.content_disposition() {
|
||||||
|
println!("{:?}", content_type);
|
||||||
|
if let Some(filename) = content_type.get_filename() {
|
||||||
|
println!("Name: {:?}", filename);
|
||||||
|
file_name = Some(filename.to_string());
|
||||||
|
|
||||||
|
while let Some(Ok(data)) = part.next().await {
|
||||||
|
file_content.extend_from_slice(data.as_ref());
|
||||||
|
}
|
||||||
|
} else if content_type.get_name().map_or(false, |name| name == "path") {
|
||||||
|
while let Some(Ok(data)) = part.next().await {
|
||||||
|
if let Ok(path) = std::str::from_utf8(&data) {
|
||||||
|
file_path = Some(path.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = file_path.unwrap_or_else(|| dotenv::var("BASE_PATH").unwrap());
|
||||||
|
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 !full_path.is_file() {
|
||||||
|
let mut file = File::create(full_path).unwrap();
|
||||||
|
file.write_all(&file_content).unwrap();
|
||||||
|
} else {
|
||||||
|
return HttpResponse::BadRequest().body("File already exists");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return HttpResponse::BadRequest().body("Path was not valid");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return HttpResponse::BadRequest().body("No file body read");
|
||||||
|
}
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/video/generate")]
|
#[post("/video/generate")]
|
||||||
async fn generate_video(_claims: Claims, body: web::Json<ThumbnailRequest>) -> impl Responder {
|
async fn generate_video(_claims: Claims, body: web::Json<ThumbnailRequest>) -> impl Responder {
|
||||||
let filename = PathBuf::from(&body.path);
|
let filename = PathBuf::from(&body.path);
|
||||||
@@ -147,7 +198,10 @@ async fn stream_video(
|
|||||||
let playlist = &path.path;
|
let playlist = &path.path;
|
||||||
println!("Playlist: {}", playlist);
|
println!("Playlist: {}", playlist);
|
||||||
|
|
||||||
if let Ok(file) = NamedFile::open(playlist) {
|
// Extract video playlist dir to dotenv
|
||||||
|
if !playlist.starts_with("tmp") || playlist.contains("..") {
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
} else if let Ok(file) = NamedFile::open(playlist) {
|
||||||
file.into_response(&request).unwrap()
|
file.into_response(&request).unwrap()
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::NotFound().finish()
|
HttpResponse::NotFound().finish()
|
||||||
@@ -218,7 +272,7 @@ async fn create_thumbnails() {
|
|||||||
generate_video_thumbnail(entry.path(), &thumb_path);
|
generate_video_thumbnail(entry.path(), &thumb_path);
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
ext == "jpg" || ext == "jpeg" || ext == "png"
|
ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "nef"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -251,12 +305,38 @@ async fn create_thumbnails() {
|
|||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
create_thumbnails().await;
|
create_thumbnails().await;
|
||||||
|
|
||||||
|
tokio::spawn(async {
|
||||||
|
let (wtx, wrx) = channel();
|
||||||
|
let mut watcher = watcher(wtx, std::time::Duration::from_secs(10)).unwrap();
|
||||||
|
watcher
|
||||||
|
.watch(dotenv::var("BASE_PATH").unwrap(), RecursiveMode::Recursive)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let ev = wrx.recv_timeout(std::time::Duration::from_secs(5));
|
||||||
|
match ev {
|
||||||
|
Ok(event) => {
|
||||||
|
match event {
|
||||||
|
DebouncedEvent::Create(_) => create_thumbnails().await,
|
||||||
|
DebouncedEvent::Rename(_, _) => create_thumbnails().await,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Event: {:?}", e);
|
||||||
|
// break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
.service(register)
|
.service(register)
|
||||||
.service(login)
|
.service(login)
|
||||||
.service(list_photos)
|
.service(list_photos)
|
||||||
.service(get_image)
|
.service(get_image)
|
||||||
|
.service(upload_image)
|
||||||
.service(generate_video)
|
.service(generate_video)
|
||||||
.service(stream_video)
|
.service(stream_video)
|
||||||
.service(get_video_part)
|
.service(get_video_part)
|
||||||
|
|||||||
Reference in New Issue
Block a user