Image Upload #1

Merged
cameron merged 7 commits from feature/image-upload into master 2020-10-17 23:25:52 +00:00
4 changed files with 123 additions and 0 deletions
Showing only changes of commit 426c695b47 - Show all commits

35
Cargo.lock generated
View File

@@ -128,6 +128,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "actix-multipart"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3"
dependencies = [
"actix-service",
"actix-utils 2.0.0",
"actix-web",
"bytes",
"derive_more",
"futures-util",
"httparse",
"log",
"mime",
"twoway",
]
[[package]] [[package]]
name = "actix-router" name = "actix-router"
version = "0.2.4" version = "0.2.4"
@@ -1097,6 +1115,7 @@ name = "image-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-multipart",
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"bcrypt", "bcrypt",
@@ -2202,12 +2221,28 @@ dependencies = [
"trust-dns-proto", "trust-dns-proto",
] ]
[[package]]
name = "twoway"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
dependencies = [
"memchr",
"unchecked-index",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.12.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.6.0" version = "2.6.0"

View File

@@ -10,6 +10,7 @@ edition = "2018"
actix-web = "3.0" actix-web = "3.0"
actix-rt = "1.0" actix-rt = "1.0"
actix-files = "0.3.0" 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"

View File

@@ -58,6 +58,17 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
} }
}) })
.ok() .ok()
} else if let Ok(path) = path.canonicalize().and_then(|path| {
if path.starts_with(base) {
Ok(path)
} else {
Err(io::Error::new(
io::ErrorKind::Other,
"Path below base directory",
))
}
}) {
Some(path)
} else { } else {
None None
} }
@@ -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")));

View File

@@ -3,13 +3,17 @@ 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 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 crate::data::{secret_key, Claims, CreateAccountRequest, Token}; use crate::data::{secret_key, Claims, CreateAccountRequest, Token};
@@ -119,6 +123,56 @@ 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);
if let Some(mut full_path) = is_valid_path(full_path.to_str().unwrap_or("")) {
// TODO: Validate this file_name as is subject to path traversals which could lead to
// writing outside the base dir.
full_path = full_path.join(file_name.unwrap());
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);
@@ -257,6 +311,7 @@ async fn main() -> std::io::Result<()> {
.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)