mirror of
https://github.com/amigan/rustypaste-pretty.git
synced 2024-11-21 20:09:48 -05:00
feat(paste): support setting an expiry date for uploads
This commit is contained in:
parent
33977de206
commit
92c35fe6df
7 changed files with 160 additions and 21 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -821,9 +821,15 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
|
@ -1518,6 +1524,8 @@ dependencies = [
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"glob",
|
||||||
|
"humantime",
|
||||||
"infer",
|
"infer",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
@ -2125,9 +2133,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.2+wasi-snapshot-preview1"
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
|
|
|
@ -27,6 +27,8 @@ url = "2.2.2"
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
serde_regex = "1.1.0"
|
serde_regex = "1.1.0"
|
||||||
|
humantime = "2.1.0"
|
||||||
|
glob = "0.3.0"
|
||||||
|
|
||||||
[dependencies.config]
|
[dependencies.config]
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
|
@ -1,9 +1,26 @@
|
||||||
|
use crate::util;
|
||||||
use actix_web::http::header::{
|
use actix_web::http::header::{
|
||||||
ContentDisposition as ActixContentDisposition, DispositionParam, DispositionType,
|
ContentDisposition as ActixContentDisposition, DispositionParam, DispositionType,
|
||||||
};
|
};
|
||||||
|
use actix_web::http::HeaderMap;
|
||||||
use actix_web::{error, Error as ActixError};
|
use actix_web::{error, Error as ActixError};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// Custom HTTP header for expiry dates.
|
||||||
|
pub const EXPIRE: &str = "expire";
|
||||||
|
|
||||||
|
/// Parses the expiry date from the [`custom HTTP header`](EXPIRE).
|
||||||
|
pub fn parse_expiry_date(headers: &HeaderMap) -> Result<Option<u128>, ActixError> {
|
||||||
|
if let Some(expire_time) = headers.get(EXPIRE).map(|v| v.to_str().ok()).flatten() {
|
||||||
|
let timestamp = util::get_system_time()?;
|
||||||
|
let expire_time =
|
||||||
|
humantime::parse_duration(expire_time).map_err(error::ErrorInternalServerError)?;
|
||||||
|
Ok(timestamp.checked_add(expire_time).map(|t| t.as_millis()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrapper for Actix content disposition header.
|
/// Wrapper for Actix content disposition header.
|
||||||
///
|
///
|
||||||
/// Aims to parse the file data from multipart body.
|
/// Aims to parse the file data from multipart body.
|
||||||
|
@ -50,6 +67,9 @@ impl ContentDisposition {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use actix_web::http::{HeaderName, HeaderValue};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_content_disposition() -> Result<(), ActixError> {
|
fn test_content_disposition() -> Result<(), ActixError> {
|
||||||
|
@ -76,4 +96,18 @@ mod tests {
|
||||||
assert!(content_disposition.get_file_name().is_err());
|
assert!(content_disposition.get_file_name().is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expiry_date() -> Result<(), ActixError> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
HeaderName::from_static(EXPIRE),
|
||||||
|
HeaderValue::from_static("5ms"),
|
||||||
|
);
|
||||||
|
let expiry_time = parse_expiry_date(&headers)?.unwrap();
|
||||||
|
assert!(expiry_time > util::get_system_time()?.as_millis());
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
assert!(expiry_time < util::get_system_time()?.as_millis());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,3 +21,6 @@ pub mod paste;
|
||||||
|
|
||||||
/// Media type handler.
|
/// Media type handler.
|
||||||
pub mod mime;
|
pub mod mime;
|
||||||
|
|
||||||
|
/// Helper functions.
|
||||||
|
pub mod util;
|
||||||
|
|
41
src/paste.rs
41
src/paste.rs
|
@ -80,7 +80,12 @@ impl Paste {
|
||||||
///
|
///
|
||||||
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
||||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||||
pub fn store_file(&self, file_name: &str, config: &Config) -> IoResult<String> {
|
pub fn store_file(
|
||||||
|
&self,
|
||||||
|
file_name: &str,
|
||||||
|
expiry_date: Option<u128>,
|
||||||
|
config: &Config,
|
||||||
|
) -> IoResult<String> {
|
||||||
let file_type = infer::get(&self.data);
|
let file_type = infer::get(&self.data);
|
||||||
if let Some(file_type) = file_type {
|
if let Some(file_type) = file_type {
|
||||||
for mime_type in &config.paste.mime_blacklist {
|
for mime_type in &config.paste.mime_blacklist {
|
||||||
|
@ -123,13 +128,17 @@ impl Paste {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut buffer = File::create(&path)?;
|
let file_name = path
|
||||||
buffer.write_all(&self.data)?;
|
|
||||||
Ok(path
|
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|v| v.to_string_lossy())
|
.map(|v| v.to_string_lossy())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string())
|
.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes an URL to a file in upload directory.
|
/// Writes an URL to a file in upload directory.
|
||||||
|
@ -138,7 +147,7 @@ impl Paste {
|
||||||
/// - If [`random_url.enabled`] is `true`, file name is set to a pet name or random string.
|
/// - If [`random_url.enabled`] is `true`, file name is set to a pet name or random string.
|
||||||
///
|
///
|
||||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||||
pub fn store_url(&self, config: &Config) -> IoResult<String> {
|
pub fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
|
||||||
let data = str::from_utf8(&self.data)
|
let data = str::from_utf8(&self.data)
|
||||||
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
.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 url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
||||||
|
@ -147,9 +156,12 @@ impl Paste {
|
||||||
.random_url
|
.random_url
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap_or_else(|| PasteType::Url.get_dir());
|
.unwrap_or_else(|| PasteType::Url.get_dir());
|
||||||
let path = PasteType::Url
|
let mut path = PasteType::Url
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.join(&file_name);
|
.join(&file_name);
|
||||||
|
if let Some(timestamp) = expiry_date {
|
||||||
|
path.set_file_name(format!("{}.{}", file_name, timestamp));
|
||||||
|
}
|
||||||
fs::write(&path, url.to_string())?;
|
fs::write(&path, url.to_string())?;
|
||||||
Ok(file_name)
|
Ok(file_name)
|
||||||
}
|
}
|
||||||
|
@ -159,6 +171,7 @@ impl Paste {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::random::{RandomURLConfig, RandomURLType};
|
use crate::random::{RandomURLConfig, RandomURLType};
|
||||||
|
use crate::util;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -176,7 +189,7 @@ mod tests {
|
||||||
data: vec![65, 66, 67],
|
data: vec![65, 66, 67],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("test.txt", &config)?;
|
let file_name = paste.store_file("test.txt", None, &config)?;
|
||||||
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some("txt"),
|
Some("txt"),
|
||||||
|
@ -199,7 +212,7 @@ mod tests {
|
||||||
data: vec![120, 121, 122],
|
data: vec![120, 121, 122],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("random", &config)?;
|
let file_name = paste.store_file("random", None, &config)?;
|
||||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some("bin"),
|
Some("bin"),
|
||||||
|
@ -219,11 +232,11 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 116],
|
data: vec![116, 101, 115, 116],
|
||||||
type_: PasteType::Oneshot,
|
type_: PasteType::Oneshot,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("test.file", &config)?;
|
let expiry_date = util::get_system_time().unwrap().as_millis() + 100;
|
||||||
|
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
|
||||||
let file_path = PasteType::Oneshot
|
let file_path = PasteType::Oneshot
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.join(&file_name);
|
.join(format!("{}.{}", file_name, expiry_date));
|
||||||
assert_eq!("test.file", &file_name);
|
|
||||||
assert_eq!("test", fs::read_to_string(&file_path)?);
|
assert_eq!("test", fs::read_to_string(&file_path)?);
|
||||||
fs::remove_file(file_path)?;
|
fs::remove_file(file_path)?;
|
||||||
|
|
||||||
|
@ -233,7 +246,7 @@ mod tests {
|
||||||
data: url.as_bytes().to_vec(),
|
data: url.as_bytes().to_vec(),
|
||||||
type_: PasteType::Url,
|
type_: PasteType::Url,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_url(&config)?;
|
let file_name = paste.store_url(None, &config)?;
|
||||||
let file_path = PasteType::Url
|
let file_path = PasteType::Url
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.join(&file_name);
|
.join(&file_name);
|
||||||
|
@ -245,7 +258,7 @@ mod tests {
|
||||||
data: url.as_bytes().to_vec(),
|
data: url.as_bytes().to_vec(),
|
||||||
type_: PasteType::Url,
|
type_: PasteType::Url,
|
||||||
};
|
};
|
||||||
assert!(paste.store_url(&config).is_err());
|
assert!(paste.store_url(None, &config).is_err());
|
||||||
|
|
||||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||||
fs::remove_dir(paste_type.get_path(&config.server.upload_path))?;
|
fs::remove_dir(paste_type.get_path(&config.server.upload_path))?;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::auth;
|
use crate::auth;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::header::ContentDisposition;
|
use crate::header::{self, ContentDisposition};
|
||||||
use crate::mime;
|
use crate::mime;
|
||||||
use crate::paste::{Paste, PasteType};
|
use crate::paste::{Paste, PasteType};
|
||||||
|
use crate::util;
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
use actix_web::{error, get, post, web, Error, HttpRequest, HttpResponse, Responder};
|
use actix_web::{error, get, post, web, Error, HttpRequest, HttpResponse, Responder};
|
||||||
|
@ -27,11 +28,13 @@ async fn serve(
|
||||||
file: web::Path<String>,
|
file: web::Path<String>,
|
||||||
config: web::Data<Config>,
|
config: web::Data<Config>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let mut path = config.server.upload_path.join(&*file);
|
let path = config.server.upload_path.join(&*file);
|
||||||
|
let mut path = util::glob_match_file(path)?;
|
||||||
let mut paste_type = PasteType::File;
|
let mut paste_type = PasteType::File;
|
||||||
if !path.exists() || path.is_dir() {
|
if !path.exists() || path.is_dir() {
|
||||||
for type_ in &[PasteType::Url, PasteType::Oneshot] {
|
for type_ in &[PasteType::Url, PasteType::Oneshot] {
|
||||||
let alt_path = type_.get_path(&config.server.upload_path).join(&*file);
|
let alt_path = type_.get_path(&config.server.upload_path).join(&*file);
|
||||||
|
let alt_path = util::glob_match_file(alt_path)?;
|
||||||
if alt_path.exists()
|
if alt_path.exists()
|
||||||
|| path.file_name().map(|v| v.to_str()).flatten() == Some(&type_.get_dir())
|
|| path.file_name().map(|v| v.to_str()).flatten() == Some(&type_.get_dir())
|
||||||
{
|
{
|
||||||
|
@ -79,6 +82,7 @@ async fn upload(
|
||||||
let connection = request.connection_info();
|
let connection = request.connection_info();
|
||||||
let host = connection.remote_addr().unwrap_or("unknown host");
|
let host = connection.remote_addr().unwrap_or("unknown host");
|
||||||
auth::check(host, request.headers(), env::var("AUTH_TOKEN").ok())?;
|
auth::check(host, request.headers(), env::var("AUTH_TOKEN").ok())?;
|
||||||
|
let expiry_date = header::parse_expiry_date(request.headers())?;
|
||||||
let mut urls: Vec<String> = Vec::new();
|
let mut urls: Vec<String> = Vec::new();
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
let mut field = item?;
|
let mut field = item?;
|
||||||
|
@ -103,9 +107,9 @@ async fn upload(
|
||||||
};
|
};
|
||||||
let file_name = match paste_type {
|
let file_name = match paste_type {
|
||||||
PasteType::File | PasteType::Oneshot => {
|
PasteType::File | PasteType::Oneshot => {
|
||||||
paste.store_file(content.get_file_name()?, &config)?
|
paste.store_file(content.get_file_name()?, expiry_date, &config)?
|
||||||
}
|
}
|
||||||
PasteType::Url => paste.store_url(&config)?,
|
PasteType::Url => paste.store_url(expiry_date, &config)?,
|
||||||
PasteType::Trash => unreachable!(),
|
PasteType::Trash => unreachable!(),
|
||||||
};
|
};
|
||||||
log::info!("{} ({}) is uploaded from {}", file_name, bytes_unit, host);
|
log::info!("{} ({}) is uploaded from {}", file_name, bytes_unit, host);
|
||||||
|
|
75
src/util.rs
Normal file
75
src/util.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use actix_web::{error, Error as ActixError};
|
||||||
|
use glob::glob;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
/// Returns the system time as [`Duration`](Duration).
|
||||||
|
pub fn get_system_time() -> Result<Duration, ActixError> {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(error::ErrorInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first _unexpired_ path matched by a custom glob pattern.
|
||||||
|
///
|
||||||
|
/// The file extension is accepted as a timestamp that points to the expiry date.
|
||||||
|
pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||||
|
if let Some(glob_path) = glob(&format!(
|
||||||
|
"{}.[0-9]*",
|
||||||
|
path.to_str()
|
||||||
|
.ok_or_else(|| error::ErrorInternalServerError(
|
||||||
|
"file name contains invalid characters"
|
||||||
|
))?,
|
||||||
|
))
|
||||||
|
.map_err(error::ErrorInternalServerError)?
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
||||||
|
if let Some(extension) = glob_path
|
||||||
|
.extension()
|
||||||
|
.map(|v| v.to_str())
|
||||||
|
.flatten()
|
||||||
|
.map(|v| v.parse().ok())
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
if get_system_time()? < Duration::from_millis(extension) {
|
||||||
|
path = glob_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::thread;
|
||||||
|
#[test]
|
||||||
|
fn test_system_time() -> Result<(), ActixError> {
|
||||||
|
let system_time = get_system_time()?.as_millis();
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
|
assert!(system_time < get_system_time()?.as_millis());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_glob_match() -> Result<(), ActixError> {
|
||||||
|
let path = PathBuf::from(format!(
|
||||||
|
"expired.file.{}",
|
||||||
|
get_system_time()?.as_millis() + 50
|
||||||
|
));
|
||||||
|
fs::write(&path, String::new())?;
|
||||||
|
assert_eq!(path, glob_match_file(PathBuf::from("expired.file"))?);
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(75));
|
||||||
|
assert_eq!(
|
||||||
|
PathBuf::from("expired.file"),
|
||||||
|
glob_match_file(PathBuf::from("expired.file"))?
|
||||||
|
);
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue