use crate::file_kind::FileKind; use actix_multipart::{Field, Multipart}; use actix_web::{error, http::header::DispositionParam}; use async_std::{fs, fs::File, path::Path, prelude::*}; use chrono::{prelude::*, Duration}; use futures::{StreamExt, TryStreamExt}; pub(crate) struct UploadConfig { pub original_name: String, pub valid_till: DateTime<Local>, pub kind: FileKind, pub delete_on_download: bool, } pub(crate) async fn parse_multipart( mut payload: Multipart, file_id: &str, filename: &Path, max_size: Option<u64>, ) -> Result<UploadConfig, error::Error> { let mut original_name: Option<String> = None; let mut keep_for: Option<String> = None; let mut kind: Option<FileKind> = None; let mut delete_on_download = false; while let Ok(Some(field)) = payload.try_next().await { let name = get_field_name(&field)?; let name = name.as_str(); match name { "keep_for" => { keep_for = Some(parse_string(name, field).await?); } "file" => { let file_original_name = get_original_filename(&field); if file_original_name == None || file_original_name.as_deref() == Some("") { continue; } original_name = file_original_name; kind = Some(FileKind::Binary); let mut file = fs::File::create(&filename) .await .map_err(|_| error::ErrorInternalServerError("could not create file"))?; write_to_file(&mut file, field, max_size).await?; } "text" => { if original_name.is_some() { continue; } original_name = Some(format!("{}.txt", file_id)); kind = Some(FileKind::Text); let mut file = fs::File::create(&filename) .await .map_err(|_| error::ErrorInternalServerError("could not create file"))?; write_to_file(&mut file, field, max_size).await?; } "delete_on_download" => { delete_on_download = dbg!(parse_string(name, field).await?) != "false"; } _ => {} }; } let original_name = original_name.ok_or_else(|| error::ErrorBadRequest("no content found"))?; let kind = kind.ok_or_else(|| error::ErrorBadRequest("no content found"))?; if original_name.len() > 255 { return Err(error::ErrorBadRequest("filename is too long")); } let valid_till = if let Some(keep_for) = keep_for { let keep_for = keep_for.parse().map_err(|e| { error::ErrorBadRequest(format!("field keep_for is not a number: {}", e)) })?; let max_keep_for = Duration::days(31).num_seconds(); if keep_for > max_keep_for { return Err(error::ErrorBadRequest(format!( "maximum allowed validity is {} seconds, but you specified {} seconds", max_keep_for, keep_for ))); } Local::now() + Duration::seconds(keep_for) } else { Local::now() + Duration::seconds(1800) }; Ok(UploadConfig { original_name, valid_till, kind, delete_on_download, }) } fn get_field_name(field: &Field) -> Result<String, error::Error> { Ok(field .content_disposition() .ok_or(error::ParseError::Incomplete)? .get_name() .map(|s| s.to_owned()) .ok_or(error::ParseError::Incomplete)?) } async fn parse_string(name: &str, field: actix_multipart::Field) -> Result<String, error::Error> { let data = read_content(field).await?; String::from_utf8(data) .map_err(|_| error::ErrorBadRequest(format!("could not parse field {} as utf-8", name))) } async fn read_content(mut field: actix_multipart::Field) -> Result<Vec<u8>, error::Error> { let mut data = Vec::new(); while let Some(chunk) = field.next().await { data.extend(chunk.map_err(error::ErrorBadRequest)?); } Ok(data) } async fn write_to_file( file: &mut File, mut field: actix_multipart::Field, max_size: Option<u64>, ) -> Result<(), error::Error> { let mut written_bytes: u64 = 0; while let Some(chunk) = field.next().await { let chunk = chunk.map_err(error::ErrorBadRequest)?; if let Some(max_size) = max_size { written_bytes += chunk.len() as u64; if written_bytes > max_size { return Err(error::ErrorBadRequest(format!( "exceeded maximum file size of {} bytes", max_size ))); } } file.write_all(chunk.as_ref()) .await .map_err(|_| error::ErrorInternalServerError("could not write file"))?; } Ok(()) } fn get_original_filename(field: &actix_multipart::Field) -> Option<String> { field.content_disposition().and_then(|content_disposition| { content_disposition .parameters .into_iter() .find_map(|param| match param { DispositionParam::Filename(filename) => Some(filename), _ => None, }) }) }