use crate::config::Config; use crate::file::Directory; use crate::header::ContentDisposition; use crate::util; use actix_web::{error, Error}; use awc::Client; use std::convert::{TryFrom, TryInto}; use std::fs::{self, File}; use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult, Write}; use std::path::{Path, PathBuf}; use std::str; use std::sync::RwLock; use url::Url; /// Type of the data to store. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PasteType { /// Any type of file. File, /// A file that is on a remote URL. RemoteFile, /// A file that allowed to be accessed once. Oneshot, /// A file that only contains an URL. Url, } impl<'a> TryFrom<&'a ContentDisposition> for PasteType { type Error = (); fn try_from(content_disposition: &'a ContentDisposition) -> Result { if content_disposition.has_form_field("file") { Ok(Self::File) } else if content_disposition.has_form_field("remote") { Ok(Self::RemoteFile) } else if content_disposition.has_form_field("oneshot") { Ok(Self::Oneshot) } else if content_disposition.has_form_field("url") { Ok(Self::Url) } else { Err(()) } } } impl PasteType { /// Returns the corresponding directory of the paste type. pub fn get_dir(&self) -> String { match self { Self::File | Self::RemoteFile => String::new(), Self::Oneshot => String::from("oneshot"), Self::Url => String::from("url"), } } /// Returns the given path with [`directory`](Self::get_dir) adjoined. pub fn get_path(&self, path: &Path) -> PathBuf { let dir = self.get_dir(); if dir.is_empty() { path.to_path_buf() } else { path.join(dir) } } /// Returns `true` if the variant is [`Oneshot`](Self::Oneshot). pub fn is_oneshot(&self) -> bool { self == &Self::Oneshot } } /// Representation of a single paste. #[derive(Debug)] pub struct Paste { /// Data to store. pub data: Vec, /// Type of the data. pub type_: PasteType, } impl Paste { /// Writes the bytes to a file in upload directory. /// /// - If `file_name` does not have an extension, it is replaced with [`default_extension`]. /// - If `file_name` is "-", it is replaced with "stdin". /// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string. /// /// [`default_extension`]: crate::config::PasteConfig::default_extension /// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled pub fn store_file( &self, file_name: &str, expiry_date: Option, config: &Config, ) -> IoResult { let file_type = infer::get(&self.data); if let Some(file_type) = file_type { for mime_type in &config.paste.mime_blacklist { if mime_type == file_type.mime_type() { return Err(IoError::new( IoErrorKind::Other, String::from("this file type is not permitted"), )); } } } let file_name = match PathBuf::from(file_name) .file_name() .and_then(|v| v.to_str()) { Some("-") => String::from("stdin"), Some(v) => v.to_string(), None => String::from("file"), }; let mut path = self .type_ .get_path(&config.server.upload_path) .join(file_name); match path.clone().extension() { Some(extension) => { if let Some(file_name) = config.paste.random_url.generate() { path.set_file_name(file_name); path.set_extension(extension); } } None => { if let Some(file_name) = config.paste.random_url.generate() { path.set_file_name(file_name); } path.set_extension( file_type .map(|t| t.extension()) .unwrap_or(&config.paste.default_extension), ); } } let file_name = path .file_name() .map(|v| v.to_string_lossy()) .unwrap_or_default() .to_string(); if let Some(timestamp) = expiry_date { path.set_file_name(format!("{file_name}.{timestamp}")); } let mut buffer = File::create(&path)?; buffer.write_all(&self.data)?; Ok(file_name) } /// Downloads a file from URL and stores it with [`store_file`]. /// /// - File name is inferred from URL if the last URL segment is a file. /// - Same content length configuration is applied for download limit. /// - Checks SHA256 digest of the downloaded file for preventing duplication. /// - Assumes `self.data` contains a valid URL, otherwise returns an error. /// /// [`store_file`]: Self::store_file pub async fn store_remote_file( &mut self, expiry_date: Option, client: &Client, config: &RwLock, ) -> Result { let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?; let url = Url::parse(data).map_err(error::ErrorBadRequest)?; let file_name = url .path_segments() .and_then(|segments| segments.last()) .and_then(|name| if name.is_empty() { None } else { Some(name) }) .unwrap_or("file"); let mut response = client .get(url.as_str()) .send() .await .map_err(error::ErrorInternalServerError)?; let payload_limit = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? .server .max_content_length .get_bytes() .try_into() .map_err(error::ErrorInternalServerError)?; let bytes = response .body() .limit(payload_limit) .await .map_err(error::ErrorInternalServerError)? .to_vec(); let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; let bytes_checksum = util::sha256_digest(&*bytes)?; self.data = bytes; if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() { if let Some(file) = Directory::try_from(config.server.upload_path.as_path())?.get_file(bytes_checksum) { return Ok(file .path .file_name() .map(|v| v.to_string_lossy()) .unwrap_or_default() .to_string()); } } Ok(self.store_file(file_name, expiry_date, &config)?) } /// Writes an URL to a file in upload directory. /// /// - Checks if the data is a valid URL. /// - If [`random_url.enabled`] is `true`, file name is set to a pet name or random string. /// /// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled pub fn store_url(&self, expiry_date: Option, config: &Config) -> IoResult { let data = str::from_utf8(&self.data) .map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?; let url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?; let file_name = config .paste .random_url .generate() .unwrap_or_else(|| PasteType::Url.get_dir()); let mut path = PasteType::Url .get_path(&config.server.upload_path) .join(&file_name); if let Some(timestamp) = expiry_date { path.set_file_name(format!("{file_name}.{timestamp}")); } fs::write(&path, url.to_string())?; Ok(file_name) } } #[cfg(test)] mod tests { use super::*; use crate::random::{RandomURLConfig, RandomURLType}; use crate::util; use actix_web::web::Data; use awc::ClientBuilder; use byte_unit::Byte; use std::env; use std::time::Duration; #[actix_rt::test] async fn test_paste_data() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; config.paste.random_url = RandomURLConfig { enabled: true, words: Some(3), separator: Some(String::from("_")), type_: RandomURLType::PetName, ..RandomURLConfig::default() }; let paste = Paste { data: vec![65, 66, 67], type_: PasteType::File, }; let file_name = paste.store_file("test.txt", None, &config)?; assert_eq!("ABC", fs::read_to_string(&file_name)?); assert_eq!( Some("txt"), PathBuf::from(&file_name) .extension() .and_then(|v| v.to_str()) ); fs::remove_file(file_name)?; config.paste.default_extension = String::from("bin"); config.paste.random_url.enabled = false; config.paste.random_url = RandomURLConfig { enabled: true, length: Some(10), type_: RandomURLType::Alphanumeric, ..RandomURLConfig::default() }; let paste = Paste { data: vec![120, 121, 122], type_: PasteType::File, }; let file_name = paste.store_file("random", None, &config)?; assert_eq!("xyz", fs::read_to_string(&file_name)?); assert_eq!( Some("bin"), PathBuf::from(&file_name) .extension() .and_then(|v| v.to_str()) ); fs::remove_file(file_name)?; for paste_type in &[PasteType::Url, PasteType::Oneshot] { fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?; } config.paste.random_url.enabled = false; let paste = Paste { data: vec![116, 101, 115, 116], type_: PasteType::Oneshot, }; let expiry_date = util::get_system_time()?.as_millis() + 100; let file_name = paste.store_file("test.file", Some(expiry_date), &config)?; let file_path = PasteType::Oneshot .get_path(&config.server.upload_path) .join(format!("{file_name}.{expiry_date}")); assert_eq!("test", fs::read_to_string(&file_path)?); fs::remove_file(file_path)?; config.paste.random_url.enabled = true; let url = String::from("https://orhun.dev/"); let paste = Paste { data: url.as_bytes().to_vec(), type_: PasteType::Url, }; let file_name = paste.store_url(None, &config)?; let file_path = PasteType::Url .get_path(&config.server.upload_path) .join(&file_name); assert_eq!(url, fs::read_to_string(&file_path)?); fs::remove_file(file_path)?; let url = String::from("testurl.com"); let paste = Paste { data: url.as_bytes().to_vec(), type_: PasteType::Url, }; assert!(paste.store_url(None, &config).is_err()); config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte"); let url = String::from("https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg"); let mut paste = Paste { data: url.as_bytes().to_vec(), type_: PasteType::RemoteFile, }; let client_data = Data::new( ClientBuilder::new() .timeout(Duration::from_secs(30)) .finish(), ); let file_name = paste .store_remote_file(None, &client_data, &RwLock::new(config.clone())) .await?; let file_path = PasteType::RemoteFile .get_path(&config.server.upload_path) .join(file_name); assert_eq!( "8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa", util::sha256_digest(&*paste.data)? ); fs::remove_file(file_path)?; for paste_type in &[PasteType::Url, PasteType::Oneshot] { fs::remove_dir(paste_type.get_path(&config.server.upload_path))?; } Ok(()) } }