Use async for filesystem hits

Except `Directory`, as there's no async-compatible `glob` implementation
This commit is contained in:
Jake Howard 2024-02-15 22:56:35 +00:00
parent fa5105deab
commit 6a43cbd620
5 changed files with 192 additions and 150 deletions

View File

@ -16,7 +16,7 @@ include = ["src/**/*", "Cargo.*", "LICENSE", "README.md", "CHANGELOG.md"]
default = ["rustls"]
openssl = ["actix-web/openssl", "awc/openssl"]
rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime"]
[dependencies]
actix-web = { version = "4.4.1" }
@ -44,7 +44,7 @@ humantime-serde = "1.1.1"
glob = "0.3.1"
ring = "0.17.7"
hotwatch = "0.5.0"
tokio = { version = "1.35.1", optional = true }
tokio = { version = "1.35.1", features = ["fs"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uts2ts = "0.4.1"

View File

@ -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(

View File

@ -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>,
@ -179,8 +181,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)
}
@ -229,9 +231,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()
@ -240,7 +249,9 @@ impl Paste {
.to_string());
}
}
Ok(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.
@ -250,7 +261,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()))?;
@ -265,7 +276,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)
}
}
@ -298,15 +309,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),
@ -318,11 +329,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),
@ -334,11 +345,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),
@ -350,10 +361,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;
@ -361,10 +372,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 {
@ -376,15 +387,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),
@ -396,15 +407,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),
@ -416,22 +429,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;
@ -440,13 +456,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),
@ -457,20 +475,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");
@ -494,14 +512,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(())

View File

@ -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(), &timestamp).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(), &timestamp).await?;
fs::remove_file(header_filename)?;
fs::remove_file(header_filename).await?;
let serve_request = TestRequest::get()
.uri(&format!("/{header_filename}"))
.to_request();
@ -911,7 +925,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);
@ -950,7 +964,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(())
}
@ -1006,7 +1020,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(())
@ -1061,7 +1075,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}"))
@ -1088,7 +1102,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,
@ -1102,8 +1116,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;
@ -1128,7 +1142,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();
@ -1165,9 +1179,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(())
}
@ -1190,7 +1204,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,
@ -1220,7 +1234,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(())
}

View File

@ -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)?;