Use async for filesystem hits
Except `Directory`, as there's no async-compatible `glob` implementation
This commit is contained in:
parent
2037530078
commit
3554253d6b
|
@ -44,7 +44,7 @@ humantime-serde = "1.1.1"
|
|||
glob = "0.3.1"
|
||||
ring = "0.17.8"
|
||||
hotwatch = "0.5.0"
|
||||
tokio = { version = "1.36.0", optional = true }
|
||||
tokio = { version = "1.35.1", optional = true, features = ["fs"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
uts2ts = "0.4.1"
|
||||
|
|
17
src/file.rs
17
src/file.rs
|
@ -1,5 +1,4 @@
|
|||
use crate::util;
|
||||
use actix_web::{error, Error as ActixError};
|
||||
use glob::glob;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File as OsFile;
|
||||
|
@ -21,12 +20,16 @@ pub struct Directory {
|
|||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Path> for Directory {
|
||||
type Error = ActixError;
|
||||
type Error = String;
|
||||
fn try_from(directory: &'a Path) -> Result<Self, Self::Error> {
|
||||
let files = glob(directory.join("**").join("*").to_str().ok_or_else(|| {
|
||||
error::ErrorInternalServerError("directory contains invalid characters")
|
||||
})?)
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
let files = glob(
|
||||
directory
|
||||
.join("**")
|
||||
.join("*")
|
||||
.to_str()
|
||||
.ok_or_else(|| String::from("directory contains invalid characters"))?,
|
||||
)
|
||||
.map_err(|e| e.msg)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|path| !path.is_dir())
|
||||
.filter_map(|path| match OsFile::open(&path) {
|
||||
|
@ -58,7 +61,7 @@ mod tests {
|
|||
use std::ffi::OsString;
|
||||
|
||||
#[test]
|
||||
fn test_file_checksum() -> Result<(), ActixError> {
|
||||
fn test_file_checksum() -> Result<(), String> {
|
||||
assert_eq!(
|
||||
Some(OsString::from("rustypaste_logo.png").as_ref()),
|
||||
Directory::try_from(
|
||||
|
|
129
src/paste.rs
129
src/paste.rs
|
@ -5,11 +5,13 @@ 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::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
use std::sync::RwLock;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::task::spawn_blocking;
|
||||
use url::Url;
|
||||
|
||||
/// Type of the data to store.
|
||||
|
@ -92,7 +94,7 @@ impl Paste {
|
|||
///
|
||||
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||
pub fn store_file(
|
||||
pub async fn store_file(
|
||||
&self,
|
||||
file_name: &str,
|
||||
expiry_date: Option<u128>,
|
||||
|
@ -183,8 +185,8 @@ impl Paste {
|
|||
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)?;
|
||||
let mut buffer = File::create(&path).await?;
|
||||
buffer.write_all(&self.data).await?;
|
||||
Ok(file_name)
|
||||
}
|
||||
|
||||
|
@ -233,9 +235,16 @@ impl Paste {
|
|||
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)
|
||||
{
|
||||
let upload_path = config.server.upload_path.clone();
|
||||
|
||||
let directory =
|
||||
match spawn_blocking(move || Directory::try_from(upload_path.as_path())).await {
|
||||
Ok(Ok(d)) => d,
|
||||
Ok(Err(e)) => return Err(error::ErrorInternalServerError(e)),
|
||||
Err(e) => return Err(error::ErrorInternalServerError(e)),
|
||||
};
|
||||
|
||||
if let Some(file) = directory.get_file(bytes_checksum) {
|
||||
return Ok(file
|
||||
.path
|
||||
.file_name()
|
||||
|
@ -244,7 +253,9 @@ impl Paste {
|
|||
.to_string());
|
||||
}
|
||||
}
|
||||
self.store_file(file_name, expiry_date, None, &config)
|
||||
Ok(self
|
||||
.store_file(file_name, expiry_date, None, &config)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Writes an URL to a file in upload directory.
|
||||
|
@ -254,7 +265,7 @@ impl Paste {
|
|||
///
|
||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||
#[allow(deprecated)]
|
||||
pub fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
|
||||
pub async 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()))?;
|
||||
|
@ -269,7 +280,7 @@ impl Paste {
|
|||
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()).await?;
|
||||
Ok(file_name)
|
||||
}
|
||||
}
|
||||
|
@ -302,15 +313,15 @@ mod tests {
|
|||
data: vec![65, 66, 67],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("test.txt", None, None, &config)?;
|
||||
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste.store_file("test.txt", None, None, &config).await?;
|
||||
assert_eq!("ABC", fs::read_to_string(&file_name).await?);
|
||||
assert_eq!(
|
||||
Some("txt"),
|
||||
PathBuf::from(&file_name)
|
||||
.extension()
|
||||
.and_then(|v| v.to_str())
|
||||
);
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
|
@ -322,11 +333,11 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config).await?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name).await?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
assert!(file_name.starts_with("foo."));
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
|
@ -338,11 +349,11 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste.store_file(".foo.tar.gz", None, None, &config).await?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name).await?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
assert!(file_name.starts_with(".foo."));
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
|
@ -354,10 +365,10 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config).await?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name).await?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.default_extension = String::from("txt");
|
||||
config.paste.random_url = None;
|
||||
|
@ -365,10 +376,10 @@ mod tests {
|
|||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(".foo", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste.store_file(".foo", None, None, &config).await?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name).await?);
|
||||
assert_eq!(".foo.txt", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.default_extension = String::from("bin");
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
|
@ -380,15 +391,15 @@ mod tests {
|
|||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("random", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste.store_file("random", None, None, &config).await?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name).await?);
|
||||
assert_eq!(
|
||||
Some("bin"),
|
||||
PathBuf::from(&file_name)
|
||||
.extension()
|
||||
.and_then(|v| v.to_str())
|
||||
);
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
|
@ -400,15 +411,17 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header.txt".to_string()),
|
||||
&config,
|
||||
)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste
|
||||
.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header.txt".to_string()),
|
||||
&config,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name).await?);
|
||||
assert_eq!("fn_from_header.txt", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
|
@ -420,22 +433,25 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header".to_string()),
|
||||
&config,
|
||||
)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
let file_name = paste
|
||||
.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header".to_string()),
|
||||
&config,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name).await?);
|
||||
assert_eq!("fn_from_header", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::create_dir_all(
|
||||
paste_type
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path"),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
config.paste.random_url = None;
|
||||
|
@ -444,13 +460,15 @@ mod tests {
|
|||
type_: PasteType::Oneshot,
|
||||
};
|
||||
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
|
||||
let file_name = paste
|
||||
.store_file("test.file", Some(expiry_date), None, &config)
|
||||
.await?;
|
||||
let file_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(format!("{file_name}.{expiry_date}"));
|
||||
assert_eq!("test", fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
assert_eq!("test", fs::read_to_string(&file_path).await?);
|
||||
fs::remove_file(file_path).await?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
enabled: Some(true),
|
||||
|
@ -461,20 +479,20 @@ mod tests {
|
|||
data: url.as_bytes().to_vec(),
|
||||
type_: PasteType::Url,
|
||||
};
|
||||
let file_name = paste.store_url(None, &config)?;
|
||||
let file_name = paste.store_url(None, &config).await?;
|
||||
let file_path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(&file_name);
|
||||
assert_eq!(url, fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
assert_eq!(url, fs::read_to_string(&file_path).await?);
|
||||
fs::remove_file(file_path).await?;
|
||||
|
||||
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());
|
||||
assert!(paste.store_url(None, &config).await.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");
|
||||
|
@ -498,14 +516,15 @@ mod tests {
|
|||
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
||||
util::sha256_digest(&*paste.data)?
|
||||
);
|
||||
fs::remove_file(file_path)?;
|
||||
fs::remove_file(file_path).await?;
|
||||
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::remove_dir(
|
||||
paste_type
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path"),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
168
src/server.rs
168
src/server.rs
|
@ -18,10 +18,10 @@ use mime::TEXT_PLAIN_UTF_8;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use uts2ts;
|
||||
|
||||
/// Shows the landing page.
|
||||
|
@ -53,7 +53,7 @@ async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error>
|
|||
}
|
||||
if let Some(mut landing_page) = config.landing_page {
|
||||
if let Some(file) = landing_page.file {
|
||||
landing_page.text = fs::read_to_string(file).ok();
|
||||
landing_page.text = fs::read_to_string(file).await.ok();
|
||||
}
|
||||
match landing_page.text {
|
||||
Some(page) => Ok(HttpResponse::Ok()
|
||||
|
@ -89,12 +89,13 @@ async fn serve(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
||||
let mut path =
|
||||
util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?).await?;
|
||||
let mut paste_type = PasteType::File;
|
||||
if !path.exists() || path.is_dir() {
|
||||
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
|
||||
let alt_path = util::glob_match_file(alt_path)?;
|
||||
let alt_path = util::glob_match_file(alt_path).await?;
|
||||
if alt_path.exists()
|
||||
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
|
||||
{
|
||||
|
@ -128,21 +129,23 @@ async fn serve(
|
|||
file,
|
||||
util::get_system_time()?.as_millis()
|
||||
)),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
PasteType::Url => Ok(HttpResponse::Found()
|
||||
.append_header(("Location", fs::read_to_string(&path)?))
|
||||
.append_header(("Location", fs::read_to_string(&path).await?))
|
||||
.finish()),
|
||||
PasteType::OneshotUrl => {
|
||||
let resp = HttpResponse::Found()
|
||||
.append_header(("Location", fs::read_to_string(&path)?))
|
||||
.append_header(("Location", fs::read_to_string(&path).await?))
|
||||
.finish();
|
||||
fs::rename(
|
||||
&path,
|
||||
path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
@ -158,11 +161,11 @@ async fn delete(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
||||
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?).await?;
|
||||
if !path.is_file() || !path.exists() {
|
||||
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
||||
}
|
||||
match fs::remove_file(path) {
|
||||
match fs::remove_file(path).await {
|
||||
Ok(_) => info!("deleted file: {:?}", file.to_string()),
|
||||
Err(e) => {
|
||||
error!("cannot delete file: {}", e);
|
||||
|
@ -250,7 +253,8 @@ async fn upload(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
if let Some(file) = Directory::try_from(config.server.upload_path.as_path())?
|
||||
if let Some(file) = Directory::try_from(config.server.upload_path.as_path())
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.get_file(bytes_checksum)
|
||||
{
|
||||
urls.push(format!(
|
||||
|
@ -273,12 +277,14 @@ async fn upload(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
paste.store_file(
|
||||
content.get_file_name()?,
|
||||
expiry_date,
|
||||
header_filename,
|
||||
&config,
|
||||
)?
|
||||
paste
|
||||
.store_file(
|
||||
content.get_file_name()?,
|
||||
expiry_date,
|
||||
header_filename,
|
||||
&config,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
PasteType::RemoteFile => {
|
||||
paste
|
||||
|
@ -289,7 +295,7 @@ async fn upload(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
paste.store_url(expiry_date, &config)?
|
||||
paste.store_url(expiry_date, &config).await?
|
||||
}
|
||||
};
|
||||
info!(
|
||||
|
@ -334,49 +340,57 @@ async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error>
|
|||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
.clone();
|
||||
|
||||
if !config.server.expose_list.unwrap_or(false) {
|
||||
warn!("server is not configured to expose list endpoint");
|
||||
Err(error::ErrorNotFound(""))?;
|
||||
return Err(error::ErrorNotFound(""));
|
||||
}
|
||||
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
|
||||
.filter_map(|entry| {
|
||||
entry.ok().and_then(|e| {
|
||||
let metadata = match e.metadata() {
|
||||
Ok(metadata) => {
|
||||
if metadata.is_dir() {
|
||||
return None;
|
||||
}
|
||||
metadata
|
||||
}
|
||||
Err(e) => {
|
||||
error!("failed to read metadata: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut file_name = PathBuf::from(e.file_name());
|
||||
let expires_at_utc = if let Some(expiration) = file_name
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
{
|
||||
file_name.set_extension("");
|
||||
if util::get_system_time().ok()?
|
||||
> Duration::from_millis(expiration.try_into().ok()?)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(uts2ts::uts2ts(expiration / 1000).as_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(ListItem {
|
||||
file_name,
|
||||
file_size: metadata.len(),
|
||||
expires_at_utc,
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut entries: Vec<ListItem> = Vec::new();
|
||||
|
||||
let mut dir_contents = fs::read_dir(config.server.upload_path).await?;
|
||||
|
||||
let system_time = util::get_system_time()?;
|
||||
|
||||
while let Ok(Some(entry)) = dir_contents.next_entry().await {
|
||||
let metadata = match entry.metadata().await {
|
||||
Ok(metadata) if metadata.is_dir() => continue,
|
||||
Ok(metadata) => metadata,
|
||||
Err(e) => {
|
||||
error!("failed to read metadata: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut file_name = PathBuf::from(entry.file_name());
|
||||
|
||||
let expires_at_utc = match file_name
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
{
|
||||
Some(expiration) if system_time > Duration::from_millis(expiration) => continue,
|
||||
Some(expiration) => {
|
||||
file_name.set_extension("");
|
||||
Some(
|
||||
uts2ts::uts2ts(
|
||||
(expiration / 1000)
|
||||
.try_into()
|
||||
.map_err(|_| error::ErrorInternalServerError("Invalid timestamp"))?,
|
||||
)
|
||||
.as_string(),
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
entries.push(ListItem {
|
||||
file_name,
|
||||
file_size: metadata.len(),
|
||||
expires_at_utc,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(entries))
|
||||
}
|
||||
|
||||
|
@ -523,7 +537,7 @@ mod tests {
|
|||
let response = test::call_service(&app, request).await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(response.into_body(), "landing page from file").await?;
|
||||
fs::remove_file(filename)?;
|
||||
fs::remove_file(filename).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -626,7 +640,7 @@ mod tests {
|
|||
config.server.expose_list = Some(true);
|
||||
|
||||
let test_upload_dir = "test_upload";
|
||||
fs::create_dir(test_upload_dir)?;
|
||||
fs::create_dir(test_upload_dir).await?;
|
||||
config.server.upload_path = PathBuf::from(test_upload_dir);
|
||||
|
||||
let app = test::init_service(
|
||||
|
@ -657,7 +671,7 @@ mod tests {
|
|||
PathBuf::from(filename)
|
||||
);
|
||||
|
||||
fs::remove_dir_all(test_upload_dir)?;
|
||||
fs::remove_dir_all(test_upload_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -668,7 +682,7 @@ mod tests {
|
|||
config.server.expose_list = Some(true);
|
||||
|
||||
let test_upload_dir = "test_upload";
|
||||
fs::create_dir(test_upload_dir)?;
|
||||
fs::create_dir(test_upload_dir).await?;
|
||||
config.server.upload_path = PathBuf::from(test_upload_dir);
|
||||
|
||||
let app = test::init_service(
|
||||
|
@ -702,7 +716,7 @@ mod tests {
|
|||
|
||||
assert!(result.is_empty());
|
||||
|
||||
fs::remove_dir_all(test_upload_dir)?;
|
||||
fs::remove_dir_all(test_upload_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -847,7 +861,7 @@ mod tests {
|
|||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(response.into_body(), ×tamp).await?;
|
||||
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{file_name}"))
|
||||
.to_request();
|
||||
|
@ -897,7 +911,7 @@ mod tests {
|
|||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(response.into_body(), ×tamp).await?;
|
||||
|
||||
fs::remove_file(header_filename)?;
|
||||
fs::remove_file(header_filename).await?;
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{header_filename}"))
|
||||
.to_request();
|
||||
|
@ -963,7 +977,7 @@ mod tests {
|
|||
#[allow(deprecated)]
|
||||
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
||||
let test_upload_dir = "test_upload";
|
||||
fs::create_dir(test_upload_dir)?;
|
||||
fs::create_dir(test_upload_dir).await?;
|
||||
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = PathBuf::from(&test_upload_dir);
|
||||
|
@ -1002,7 +1016,7 @@ mod tests {
|
|||
|
||||
assert_eq!(first_body_bytes, second_body_bytes);
|
||||
|
||||
fs::remove_dir_all(test_upload_dir)?;
|
||||
fs::remove_dir_all(test_upload_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1058,7 +1072,7 @@ mod tests {
|
|||
.map_err(error::ErrorInternalServerError)?
|
||||
.next()
|
||||
{
|
||||
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?;
|
||||
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -1113,7 +1127,7 @@ mod tests {
|
|||
util::sha256_digest(&*body_bytes)?
|
||||
);
|
||||
|
||||
fs::remove_file(file_name)?;
|
||||
fs::remove_file(file_name).await?;
|
||||
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{file_name}"))
|
||||
|
@ -1140,7 +1154,7 @@ mod tests {
|
|||
let url_upload_path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&url_upload_path)?;
|
||||
fs::create_dir_all(&url_upload_path).await?;
|
||||
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
|
@ -1154,8 +1168,8 @@ mod tests {
|
|||
let response = test::call_service(&app, serve_request).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
fs::remove_file(url_upload_path.join("url"))?;
|
||||
fs::remove_dir(url_upload_path)?;
|
||||
fs::remove_file(url_upload_path.join("url")).await?;
|
||||
fs::remove_dir(url_upload_path).await?;
|
||||
|
||||
let serve_request = TestRequest::get().uri("/url").to_request();
|
||||
let response = test::call_service(&app, serve_request).await;
|
||||
|
@ -1180,7 +1194,7 @@ mod tests {
|
|||
let oneshot_upload_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&oneshot_upload_path)?;
|
||||
fs::create_dir_all(&oneshot_upload_path).await?;
|
||||
|
||||
let file_name = "oneshot.txt";
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
|
@ -1217,9 +1231,9 @@ mod tests {
|
|||
.map_err(error::ErrorInternalServerError)?
|
||||
.next()
|
||||
{
|
||||
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?;
|
||||
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?).await?;
|
||||
}
|
||||
fs::remove_dir(oneshot_upload_path)?;
|
||||
fs::remove_dir(oneshot_upload_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1242,7 +1256,7 @@ mod tests {
|
|||
let url_upload_path = PasteType::OneshotUrl
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&url_upload_path)?;
|
||||
fs::create_dir_all(&url_upload_path).await?;
|
||||
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
|
@ -1272,7 +1286,7 @@ mod tests {
|
|||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
|
||||
// Cleanup
|
||||
fs::remove_dir_all(url_upload_path)?;
|
||||
fs::remove_dir_all(url_upload_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
24
src/util.rs
24
src/util.rs
|
@ -10,6 +10,7 @@ use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
/// Regex for matching the timestamp extension of a path.
|
||||
pub static TIMESTAMP_EXTENSION_REGEX: Lazy<Regex> = lazy_regex!(r#"\.[0-9]{10,}$"#);
|
||||
|
@ -24,7 +25,7 @@ pub fn get_system_time() -> Result<Duration, ActixError> {
|
|||
/// 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> {
|
||||
pub async fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||
path = PathBuf::from(
|
||||
TIMESTAMP_EXTENSION_REGEX
|
||||
.replacen(
|
||||
|
@ -36,10 +37,15 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
|||
)
|
||||
.to_string(),
|
||||
);
|
||||
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.last()
|
||||
{
|
||||
|
||||
let path_string = path.to_string_lossy().into_owned();
|
||||
let glob_match = match spawn_blocking(move || glob(&format!("{}.[0-9]*", path_string))).await {
|
||||
Ok(Ok(m)) => m.last(),
|
||||
Ok(Err(e)) => return Err(error::ErrorInternalServerError(e)),
|
||||
Err(e) => return Err(error::ErrorInternalServerError(e)),
|
||||
};
|
||||
|
||||
if let Some(glob_path) = glob_match {
|
||||
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
||||
if let Some(extension) = glob_path
|
||||
.extension()
|
||||
|
@ -145,19 +151,19 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glob_match() -> Result<(), ActixError> {
|
||||
#[actix_rt::test]
|
||||
async fn test_glob_match() -> Result<(), ActixError> {
|
||||
let path = PathBuf::from(format!(
|
||||
"expired.file1.{}",
|
||||
get_system_time()?.as_millis() + 50
|
||||
));
|
||||
fs::write(&path, String::new())?;
|
||||
assert_eq!(path, glob_match_file(PathBuf::from("expired.file1"))?);
|
||||
assert_eq!(path, glob_match_file(PathBuf::from("expired.file1")).await?);
|
||||
|
||||
thread::sleep(Duration::from_millis(75));
|
||||
assert_eq!(
|
||||
PathBuf::from("expired.file1"),
|
||||
glob_match_file(PathBuf::from("expired.file1"))?
|
||||
glob_match_file(PathBuf::from("expired.file1")).await?
|
||||
);
|
||||
fs::remove_file(path)?;
|
||||
|
||||
|
|
Loading…
Reference in New Issue