360 lines
12 KiB
Rust
360 lines
12 KiB
Rust
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<Self, Self::Error> {
|
|
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<u8>,
|
|
/// 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<u128>,
|
|
config: &Config,
|
|
) -> IoResult<String> {
|
|
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<u128>,
|
|
client: &Client,
|
|
config: &RwLock<Config>,
|
|
) -> Result<String, Error> {
|
|
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<u128>, config: &Config) -> IoResult<String> {
|
|
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(())
|
|
}
|
|
}
|