diff options
author | boasting-squirrel <boasting.squirrel@gmail.com> | 2019-04-06 14:39:12 +0000 |
---|---|---|
committer | boasting-squirrel <boasting.squirrel@gmail.com> | 2019-04-06 14:39:12 +0000 |
commit | 134c7397d4a8686fd091119c7ab2bcc06f0247e8 (patch) | |
tree | 8ef74dcb80bd93760af8edb3e2f534c6b6856331 /src/file_upload.rs | |
parent | Use serde lowercase for SortingMethods enum (diff) | |
parent | Merge pull request #58 from vojta7/file_uploading (diff) | |
download | miniserve-134c7397d4a8686fd091119c7ab2bcc06f0247e8.tar.gz miniserve-134c7397d4a8686fd091119c7ab2bcc06f0247e8.zip |
Merged upload + adapted it to the new design
Diffstat (limited to 'src/file_upload.rs')
-rw-r--r-- | src/file_upload.rs | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/src/file_upload.rs b/src/file_upload.rs new file mode 100644 index 0000000..273d12c --- /dev/null +++ b/src/file_upload.rs @@ -0,0 +1,152 @@ +use crate::errors::FileUploadErrorKind; +use crate::renderer::file_upload_error; +use actix_web::{ + dev, http::header, multipart, FromRequest, FutureResponse, HttpMessage, HttpRequest, + HttpResponse, Query, +}; +use futures::{future, Future, Stream}; +use serde::Deserialize; +use std::{ + fs, + io::Write, + path::{Component, PathBuf}, +}; + +/// Query parameters +#[derive(Debug, Deserialize)] +struct QueryParameters { + path: PathBuf, +} + +/// Create future to save file. +fn save_file( + field: multipart::Field<dev::Payload>, + file_path: PathBuf, + overwrite_files: bool, +) -> Box<Future<Item = i64, Error = FileUploadErrorKind>> { + if !overwrite_files && file_path.exists() { + return Box::new(future::err(FileUploadErrorKind::FileExist)); + } + let mut file = match std::fs::File::create(file_path) { + Ok(file) => file, + Err(e) => { + return Box::new(future::err(FileUploadErrorKind::IOError(e))); + } + }; + Box::new( + field + .map_err(FileUploadErrorKind::MultipartError) + .fold(0i64, move |acc, bytes| { + let rt = file + .write_all(bytes.as_ref()) + .map(|_| acc + bytes.len() as i64) + .map_err(FileUploadErrorKind::IOError); + future::result(rt) + }), + ) +} + +/// Create new future to handle file as multipart data. +fn handle_multipart( + item: multipart::MultipartItem<dev::Payload>, + mut file_path: PathBuf, + overwrite_files: bool, +) -> Box<Stream<Item = i64, Error = FileUploadErrorKind>> { + match item { + multipart::MultipartItem::Field(field) => { + let filename = field + .headers() + .get(header::CONTENT_DISPOSITION) + .ok_or(FileUploadErrorKind::ParseError) + .and_then(|cd| { + header::ContentDisposition::from_raw(cd) + .map_err(|_| FileUploadErrorKind::ParseError) + }) + .and_then(|content_disposition| { + content_disposition + .get_filename() + .ok_or(FileUploadErrorKind::ParseError) + .map(String::from) + }); + let err = |e: FileUploadErrorKind| Box::new(future::err(e).into_stream()); + match filename { + Ok(f) => { + match fs::metadata(&file_path) { + Ok(metadata) => { + if !metadata.is_dir() || metadata.permissions().readonly() { + return err(FileUploadErrorKind::InsufficientPermissions); + } + } + Err(_) => { + return err(FileUploadErrorKind::InsufficientPermissions); + } + } + file_path = file_path.join(f); + Box::new(save_file(field, file_path, overwrite_files).into_stream()) + } + Err(e) => err(e), + } + } + multipart::MultipartItem::Nested(mp) => Box::new( + mp.map_err(FileUploadErrorKind::MultipartError) + .map(move |item| handle_multipart(item, file_path.clone(), overwrite_files)) + .flatten(), + ), + } +} + +/// Handle incoming request to upload file. +/// Target file path is expected as path parameter in URI and is interpreted as relative from +/// server root directory. Any path which will go outside of this directory is considered +/// invalid. +/// This method returns future. +pub fn upload_file(req: &HttpRequest<crate::MiniserveConfig>) -> FutureResponse<HttpResponse> { + let app_root_dir = req.state().path.clone().canonicalize().unwrap(); + let path = match Query::<QueryParameters>::extract(req) { + Ok(query) => { + if let Ok(stripped_path) = query.path.strip_prefix(Component::RootDir) { + stripped_path.to_owned() + } else { + query.path.clone() + } + } + Err(_) => { + return Box::new(future::ok( + HttpResponse::BadRequest().body("Unspecified parameter path"), + )) + } + }; + // this is really ugly I will try to think about something smarter + let return_path: String = req.headers()[header::REFERER] + .clone() + .to_str() + .unwrap_or("/") + .to_owned(); + let r_p2 = return_path.clone(); + + // If the target path is under the app root directory, save the file. + let target_dir = match &app_root_dir.clone().join(path.clone()).canonicalize() { + Ok(path) if path.starts_with(&app_root_dir) => path.clone(), + _ => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), + }; + let overwrite_files = req.state().overwrite_files; + Box::new( + req.multipart() + .map_err(FileUploadErrorKind::MultipartError) + .map(move |item| handle_multipart(item, target_dir.clone(), overwrite_files)) + .flatten() + .collect() + .map(move |_| { + HttpResponse::TemporaryRedirect() + .header(header::LOCATION, return_path.to_string()) + .finish() + }) + .or_else(move |e| { + let error_description = format!("{}", e); + future::ok( + HttpResponse::BadRequest() + .body(file_upload_error(&error_description, &r_p2.clone()).into_string()), + ) + }), + ) +} |