Compare commits

...

21 Commits

Author SHA1 Message Date
Jake Howard 2a25c78976
Merge 3fccfd61c8 into 15e3619479 2024-04-03 22:03:33 +00:00
Orhun Parmaksız 3fccfd61c8
Merge branch 'master' into feature/full-async 2024-04-04 01:03:25 +03:00
dependabot[bot] 15e3619479
chore(deps): bump tokio from 1.36.0 to 1.37.0 (#268)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-04-04 00:09:29 +03:00
Orhun Parmaksız 2037530078
refactor(ci): provide codecov token via env
https://github.com/codecov/codecov-action/issues/1292
2024-04-04 00:03:57 +03:00
Orhun Parmaksız 44c07a3eb6
chore(release): prepare for v0.15.0 2024-03-27 23:03:10 +03:00
Orhun Parmaksız 64d783bd0d
chore(deps): bump all dependencies 2024-03-27 22:49:10 +03:00
Orhun Parmaksız 1fd561f869
docs(readme): add packaging status badge 2024-03-27 17:39:35 +03:00
dependabot[bot] c6d6da6296
chore(deps): bump codecov/codecov-action from 3 to 4 (#237)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:35:54 +03:00
Orhun Parmaksız 54e2ddc91a
chore(deps): bump serde from 1.0.196 to 1.0.197 2024-03-27 17:35:12 +03:00
dependabot[bot] 8e505c0da8
chore(deps): bump regex from 1.10.3 to 1.10.4 (#267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:34:04 +03:00
Orhun Parmaksız 0f0ba72305
chore(deps): bump shuttle dependencies 2024-03-27 17:33:23 +03:00
dependabot[bot] 77e97573ef
chore(deps): bump tokio from 1.35.1 to 1.36.0 (#242)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:27:22 +03:00
Jake Howard 18a1abf771
Run tests in isolated directory
This prevents tests from conflicting with eachother if they don't clean up after themselves correctly.
2024-03-23 11:40:06 +00:00
Orhun Parmaksız 49eadb63d5
refactor(tests): split tests for readability 2024-03-21 17:23:21 +03:00
Jake Howard dd2a635574
Merge remote-tracking branch 'origin/master' into feature/full-async 2024-03-15 18:52:24 +00:00
Jake Howard 46210d22f3
Ensure config read locks are dropped when expected 2024-03-08 14:45:28 +00:00
Orhun Parmaksız 8cd16369f0
Merge branch 'master' into feature/full-async 2024-03-08 14:24:35 +01:00
Jake Howard 3547c079e7 Use correct `RwLock` when creating server
This required a little refactor, but the code is now much cleaner, and correctly handles updating the config
2024-03-05 22:44:44 +00:00
Jake Howard fc9e493408 Correctly get config when extracting tokens 2024-02-28 20:01:25 +00:00
Jake Howard 07fb759b5d Use async lock for config 2024-02-25 15:13:14 +00:00
Jake Howard 6a43cbd620 Use async for filesystem hits
Except `Directory`, as there's no async-compatible `glob` implementation
2024-02-15 22:56:35 +00:00
12 changed files with 799 additions and 996 deletions

View File

@ -37,14 +37,15 @@ jobs:
env: env:
OUT_DIR: target OUT_DIR: target
- name: Upload reports to codecov - name: Upload reports to codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
name: code-coverage-report name: code-coverage-report
file: lcov.info file: lcov.info
flags: unit-tests flags: unit-tests
fail_ci_if_error: true fail_ci_if_error: true
verbose: true verbose: true
token: ${{ secrets.CODECOV_TOKEN }} env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
fixtures: fixtures:
strategy: strategy:

View File

@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.15.0] - 2024-03-27
### Added
- Allow to override filename when using `random_url` by @tessus in [#233](https://github.com/orhun/rustypaste/pull/233)
Now you can use the `filename` header to override the name of the uploaded file.
For example:
```sh
curl -F "file=@x.txt" -H "filename:override.txt" http://localhost:8000
```
Even if `random_url` is set, the filename will be override.txt
[`rustypaste-cli`](https://github.com/orhun/rustypaste-cli) also has a new argument for overriding the file name:
```sh
rpaste -n filename-on-server.txt awesome.txt
```
- Use more specific HTTP status codes by @tessus in [#262](https://github.com/orhun/rustypaste/pull/262)
`rustypaste` now returns more appropriate status codes in the following 2 cases (instead of a generic 500 code):
- If the mime type is on the blacklist: `UnsupportedMediaType` (415)
- If the file already exists: `Conflict` (409)
### Changed
- Do path joins more safely by @RealOrangeOne in [#247](https://github.com/orhun/rustypaste/pull/247)
- Gracefully exit when there is no config file found by @orhun
- Switch to cargo-llvm-cov for code coverage by @orhun in [#260](https://github.com/orhun/rustypaste/pull/260)
- Replace unmaintained action by @tessus in [#266](https://github.com/orhun/rustypaste/pull/266)
- Set up mergify by @orhun
- Apply clippy suggestions by @orhun
- Update funding options by @orhun
- Update the copyright years by @orhun
- Bump dependencies
### Fixed
- Improve logging for deleted file by @tessus in [#235](https://github.com/orhun/rustypaste/pull/235)
- Fix deployment by @tessus in [#236](https://github.com/orhun/rustypaste/pull/236)
- Return the correct file on multiple files with same name by @tessus in [#234](https://github.com/orhun/rustypaste/pull/234)
- Update the hash of the example file by @tessus in [#254](https://github.com/orhun/rustypaste/pull/254)
- Error on upload with the same filename by @tessus in [#258](https://github.com/orhun/rustypaste/pull/258)
### New Contributors
- @RealOrangeOne made their first contribution in [#247](https://github.com/orhun/rustypaste/pull/247)
## [0.14.4] - 2023-12-20 ## [0.14.4] - 2023-12-20
### Removed ### Removed

1027
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rustypaste" name = "rustypaste"
version = "0.14.4" version = "0.15.0"
edition = "2021" edition = "2021"
description = "A minimal file upload/pastebin service" description = "A minimal file upload/pastebin service"
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"] authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
@ -16,17 +16,17 @@ include = ["src/**/*", "Cargo.*", "LICENSE", "README.md", "CHANGELOG.md"]
default = ["rustls"] default = ["rustls"]
openssl = ["actix-web/openssl", "awc/openssl"] openssl = ["actix-web/openssl", "awc/openssl"]
rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"] 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] [dependencies]
actix-web = { version = "4.4.1" } actix-web = { version = "4.5.1" }
actix-web-grants = { version = "4.0.3" } actix-web-grants = { version = "4.0.3" }
actix-multipart = "0.6.1" actix-multipart = "0.6.1"
actix-files = "0.6.5" actix-files = "0.6.5"
shuttle-actix-web = { version = "0.35.1", optional = true } shuttle-actix-web = { version = "0.42.0", optional = true }
shuttle-runtime = { version = "0.35.1", optional = true } shuttle-runtime = { version = "0.42.0", optional = true }
awc = { version = "3.4.0" } awc = { version = "3.4.0" }
serde = "1.0.196" serde = "1.0.197"
futures-util = "0.3.30" futures-util = "0.3.30"
petname = { version = "1.1.3", default-features = false, features = [ petname = { version = "1.1.3", default-features = false, features = [
"std_rng", "std_rng",
@ -36,7 +36,7 @@ rand = "0.8.5"
dotenvy = "0.15.7" dotenvy = "0.15.7"
url = "2.5.0" url = "2.5.0"
mime = "0.3.17" mime = "0.3.17"
regex = "1.10.3" regex = "1.10.4"
serde_regex = "1.1.0" serde_regex = "1.1.0"
lazy-regex = "3.1.0" lazy-regex = "3.1.0"
humantime = "2.1.0" humantime = "2.1.0"
@ -44,7 +44,7 @@ humantime-serde = "1.1.1"
glob = "0.3.1" glob = "0.3.1"
ring = "0.17.8" ring = "0.17.8"
hotwatch = "0.5.0" hotwatch = "0.5.0"
tokio = { version = "1.35.1", optional = true } tokio = { version = "1.37.0", features = ["fs"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uts2ts = "0.4.1" uts2ts = "0.4.1"
@ -65,6 +65,7 @@ default-features = false
[dev-dependencies] [dev-dependencies]
actix-rt = "2.9.0" actix-rt = "2.9.0"
tempfile = "3.10.1"
[profile.dev] [profile.dev]
opt-level = 0 opt-level = 0

View File

@ -99,6 +99,13 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
## Installation ## Installation
<details>
<summary>Packaging status</summary>
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustypaste.svg)](https://repology.org/project/rustypaste/versions)
</details>
### From crates.io ### From crates.io
```sh ```sh

View File

@ -5,7 +5,7 @@ use actix_web::http::Method;
use actix_web::middleware::ErrorHandlerResponse; use actix_web::middleware::ErrorHandlerResponse;
use actix_web::{error, web, Error}; use actix_web::{error, web, Error};
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::RwLock; use tokio::sync::RwLock;
/// Extracts the tokens from the authorization header by token type. /// Extracts the tokens from the authorization header by token type.
/// ///
@ -14,8 +14,8 @@ pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<Token
let config = req let config = req
.app_data::<web::Data<RwLock<Config>>>() .app_data::<web::Data<RwLock<Config>>>()
.map(|cfg| cfg.read()) .map(|cfg| cfg.read())
.and_then(Result::ok) .ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?; .await;
let mut user_tokens = HashSet::with_capacity(2); let mut user_tokens = HashSet::with_capacity(2);

View File

@ -120,13 +120,20 @@ pub struct PasteConfig {
pub delete_expired_files: Option<CleanupConfig>, pub delete_expired_files: Option<CleanupConfig>,
} }
/// Default interval for cleanup
pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
const fn get_default_cleanup_interval() -> Duration {
DEFAULT_CLEANUP_INTERVAL
}
/// Cleanup configuration. /// Cleanup configuration.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CleanupConfig { pub struct CleanupConfig {
/// Enable cleaning up. /// Enable cleaning up.
pub enabled: bool, pub enabled: bool,
/// Interval between clean-ups. /// Interval between clean-ups.
#[serde(default, with = "humantime_serde")] #[serde(default = "get_default_cleanup_interval", with = "humantime_serde")]
pub interval: Duration, pub interval: Duration,
} }

View File

@ -1,5 +1,4 @@
use crate::util; use crate::util;
use actix_web::{error, Error as ActixError};
use glob::glob; use glob::glob;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fs::File as OsFile; use std::fs::File as OsFile;
@ -21,12 +20,16 @@ pub struct Directory {
} }
impl<'a> TryFrom<&'a Path> for Directory { impl<'a> TryFrom<&'a Path> for Directory {
type Error = ActixError; type Error = String;
fn try_from(directory: &'a Path) -> Result<Self, Self::Error> { fn try_from(directory: &'a Path) -> Result<Self, Self::Error> {
let files = glob(directory.join("**").join("*").to_str().ok_or_else(|| { let files = glob(
error::ErrorInternalServerError("directory contains invalid characters") directory
})?) .join("**")
.map_err(error::ErrorInternalServerError)? .join("*")
.to_str()
.ok_or_else(|| String::from("directory contains invalid characters"))?,
)
.map_err(|e| e.msg)?
.filter_map(Result::ok) .filter_map(Result::ok)
.filter(|path| !path.is_dir()) .filter(|path| !path.is_dir())
.filter_map(|path| match OsFile::open(&path) { .filter_map(|path| match OsFile::open(&path) {
@ -58,7 +61,7 @@ mod tests {
use std::ffi::OsString; use std::ffi::OsString;
#[test] #[test]
fn test_file_checksum() -> Result<(), ActixError> { fn test_file_checksum() -> Result<(), String> {
assert_eq!( assert_eq!(
Some(OsString::from("rustypaste_logo.png").as_ref()), Some(OsString::from("rustypaste_logo.png").as_ref()),
Directory::try_from( Directory::try_from(

View File

@ -5,7 +5,7 @@ use actix_web::{App, HttpServer};
use awc::ClientBuilder; use awc::ClientBuilder;
use hotwatch::notify::event::ModifyKind; use hotwatch::notify::event::ModifyKind;
use hotwatch::{Event, EventKind, Hotwatch}; use hotwatch::{Event, EventKind, Hotwatch};
use rustypaste::config::{Config, ServerConfig}; use rustypaste::config::{Config, DEFAULT_CLEANUP_INTERVAL};
use rustypaste::middleware::ContentLengthLimiter; use rustypaste::middleware::ContentLengthLimiter;
use rustypaste::paste::PasteType; use rustypaste::paste::PasteType;
use rustypaste::server; use rustypaste::server;
@ -15,9 +15,9 @@ use std::env;
use std::fs; use std::fs;
use std::io::Result as IoResult; use std::io::Result as IoResult;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{mpsc, RwLock};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use tokio::sync::RwLock;
#[cfg(not(feature = "shuttle"))] #[cfg(not(feature = "shuttle"))]
use tracing_subscriber::{ use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter,
@ -38,7 +38,7 @@ extern crate tracing;
/// * initializes the logger /// * initializes the logger
/// * creates the necessary directories /// * creates the necessary directories
/// * spawns the threads /// * spawns the threads
fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig, Hotwatch)> { async fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, Hotwatch)> {
// Load the .env file. // Load the .env file.
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
@ -61,6 +61,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
} }
None => config_folder.join("config.toml"), None => config_folder.join("config.toml"),
}; };
if !config_path.exists() { if !config_path.exists() {
error!( error!(
"{} is not found, please provide a configuration file.", "{} is not found, please provide a configuration file.",
@ -68,17 +69,15 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
); );
std::process::exit(1); std::process::exit(1);
} }
let config = Config::parse(&config_path).expect("failed to parse config"); let config = Config::parse(&config_path).expect("failed to parse config");
trace!("{:#?}", config); trace!("{:#?}", config);
config.warn_deprecation(); config.warn_deprecation();
let server_config = config.server.clone();
let paste_config = RwLock::new(config.paste.clone());
let (config_sender, config_receiver) = mpsc::channel::<Config>();
// Create necessary directories. // Create necessary directories.
fs::create_dir_all(&server_config.upload_path)?; fs::create_dir_all(&config.server.upload_path)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] { for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
fs::create_dir_all(paste_type.get_path(&server_config.upload_path)?)?; fs::create_dir_all(paste_type.get_path(&config.server.upload_path)?)?;
} }
// Set up a watcher for the configuration file changes. // Set up a watcher for the configuration file changes.
@ -91,82 +90,71 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
) )
.expect("failed to initialize configuration file watcher"); .expect("failed to initialize configuration file watcher");
let config_lock = Data::new(RwLock::new(config));
// Hot-reload the configuration file. // Hot-reload the configuration file.
let config = Data::new(RwLock::new(config)); let config_watcher_config = config_lock.clone();
let cloned_config = Data::clone(&config);
let config_watcher = move |event: Event| { let config_watcher = move |event: Event| {
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) = if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
(event.kind, event.paths.first()) (event.kind, event.paths.first())
{ {
info!("Reloading configuration");
match Config::parse(path) { match Config::parse(path) {
Ok(config) => match cloned_config.write() { Ok(new_config) => {
Ok(mut cloned_config) => { let mut locked_config = config_watcher_config.blocking_write();
*cloned_config = config.clone(); *locked_config = new_config;
info!("Configuration has been updated."); info!("Configuration has been updated.");
if let Err(e) = config_sender.send(config) { locked_config.warn_deprecation();
error!("Failed to send config for the cleanup routine: {}", e) }
}
cloned_config.warn_deprecation();
}
Err(e) => {
error!("Failed to acquire config: {}", e);
}
},
Err(e) => { Err(e) => {
error!("Failed to update config: {}", e); error!("Failed to update config: {}", e);
} }
} }
} }
}; };
hotwatch hotwatch
.watch(&config_path, config_watcher) .watch(&config_path, config_watcher)
.unwrap_or_else(|_| panic!("failed to watch {config_path:?}")); .unwrap_or_else(|_| panic!("failed to watch {config_path:?}"));
// Create a thread for cleaning up expired files. // Create a thread for cleaning up expired files.
let upload_path = server_config.upload_path.clone(); let expired_files_config = config_lock.clone();
let mut cleanup_interval = DEFAULT_CLEANUP_INTERVAL;
thread::spawn(move || loop { thread::spawn(move || loop {
let mut enabled = false; // Additional context block to ensure the config lock is dropped
if let Some(ref cleanup_config) = paste_config
.read()
.ok()
.and_then(|v| v.delete_expired_files.clone())
{ {
if cleanup_config.enabled { let locked_config = expired_files_config.blocking_read();
debug!("Running cleanup..."); let upload_path = locked_config.server.upload_path.clone();
for file in util::get_expired_files(&upload_path) {
match fs::remove_file(&file) { if let Some(ref cleanup_config) = locked_config.paste.delete_expired_files {
Ok(()) => info!("Removed expired file: {:?}", file), if cleanup_config.enabled {
Err(e) => error!("Cannot remove expired file: {}", e), debug!("Running cleanup...");
for file in util::get_expired_files(&upload_path) {
match fs::remove_file(&file) {
Ok(()) => info!("Removed expired file: {:?}", file),
Err(e) => error!("Cannot remove expired file: {}", e),
}
} }
} cleanup_interval = cleanup_config.interval;
thread::sleep(cleanup_config.interval);
}
enabled = cleanup_config.enabled;
}
if let Some(new_config) = if enabled {
config_receiver.try_recv().ok()
} else {
config_receiver.recv().ok()
} {
match paste_config.write() {
Ok(mut paste_config) => {
*paste_config = new_config.paste;
}
Err(e) => {
error!("Failed to update config for the cleanup routine: {}", e);
} }
} }
} }
thread::sleep(cleanup_interval);
}); });
Ok((config, server_config, hotwatch)) Ok((config_lock, hotwatch))
} }
#[cfg(not(feature = "shuttle"))] #[cfg(not(feature = "shuttle"))]
#[actix_web::main] #[actix_web::main]
async fn main() -> IoResult<()> { async fn main() -> IoResult<()> {
// Set up the application. // Set up the application.
let (config, server_config, _hotwatch) = setup(&PathBuf::new())?; let (config, _hotwatch) = setup(&PathBuf::new()).await?;
// Extra context block ensures the lock is stopped
let server_config = { config.read().await.server.clone() };
// Create an HTTP server. // Create an HTTP server.
let mut http_server = HttpServer::new(move || { let mut http_server = HttpServer::new(move || {
@ -203,7 +191,10 @@ async fn main() -> IoResult<()> {
#[shuttle_runtime::main] #[shuttle_runtime::main]
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> { async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
// Set up the application. // Set up the application.
let (config, server_config, _hotwatch) = setup(Path::new("shuttle"))?; let (config, _hotwatch) = setup(Path::new("shuttle"))?;
// Extra context block ensures the lock is stopped
let server_config = { config.read().await.server.clone() };
// Create the service. // Create the service.
let service_config = move |cfg: &mut ServiceConfig| { let service_config = move |cfg: &mut ServiceConfig| {

View File

@ -5,11 +5,12 @@ use crate::util;
use actix_web::{error, Error}; use actix_web::{error, Error};
use awc::Client; use awc::Client;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use std::fs::{self, File}; use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str; use std::str;
use std::sync::RwLock; use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
use tokio::task::spawn_blocking;
use url::Url; use url::Url;
/// Type of the data to store. /// Type of the data to store.
@ -92,7 +93,7 @@ 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( pub async fn store_file(
&self, &self,
file_name: &str, file_name: &str,
expiry_date: Option<u128>, expiry_date: Option<u128>,
@ -176,6 +177,7 @@ impl Paste {
.unwrap_or_default() .unwrap_or_default()
.to_string(); .to_string();
let file_path = util::glob_match_file(path.clone()) let file_path = util::glob_match_file(path.clone())
.await
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?; .map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
if file_path.is_file() && file_path.exists() { if file_path.is_file() && file_path.exists() {
return Err(error::ErrorConflict("file already exists\n")); return Err(error::ErrorConflict("file already exists\n"));
@ -183,8 +185,8 @@ impl Paste {
if let Some(timestamp) = expiry_date { if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}")); path.set_file_name(format!("{file_name}.{timestamp}"));
} }
let mut buffer = File::create(&path)?; let mut buffer = File::create(&path).await?;
buffer.write_all(&self.data)?; buffer.write_all(&self.data).await?;
Ok(file_name) Ok(file_name)
} }
@ -200,7 +202,7 @@ impl Paste {
&mut self, &mut self,
expiry_date: Option<u128>, expiry_date: Option<u128>,
client: &Client, client: &Client,
config: &RwLock<Config>, config: &Config,
) -> Result<String, Error> { ) -> Result<String, Error> {
let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?; let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?;
let url = Url::parse(data).map_err(error::ErrorBadRequest)?; let url = Url::parse(data).map_err(error::ErrorBadRequest)?;
@ -215,8 +217,6 @@ impl Paste {
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let payload_limit = config let payload_limit = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.server .server
.max_content_length .max_content_length
.try_into() .try_into()
@ -227,15 +227,19 @@ impl Paste {
.await .await
.map_err(error::ErrorInternalServerError)? .map_err(error::ErrorInternalServerError)?
.to_vec(); .to_vec();
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let bytes_checksum = util::sha256_digest(&*bytes)?; let bytes_checksum = util::sha256_digest(&*bytes)?;
self.data = bytes; self.data = bytes;
if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() { if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() {
if let Some(file) = let upload_path = config.server.upload_path.clone();
Directory::try_from(config.server.upload_path.as_path())?.get_file(bytes_checksum)
{ 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 return Ok(file
.path .path
.file_name() .file_name()
@ -244,7 +248,7 @@ impl Paste {
.to_string()); .to_string());
} }
} }
self.store_file(file_name, expiry_date, None, &config) self.store_file(file_name, expiry_date, None, config).await
} }
/// Writes an URL to a file in upload directory. /// Writes an URL to a file in upload directory.
@ -254,7 +258,7 @@ impl Paste {
/// ///
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled /// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
#[allow(deprecated)] #[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) 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()))?;
@ -269,7 +273,7 @@ impl Paste {
if let Some(timestamp) = expiry_date { if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}")); path.set_file_name(format!("{file_name}.{timestamp}"));
} }
fs::write(&path, url.to_string())?; fs::write(&path, url.to_string()).await?;
Ok(file_name) Ok(file_name)
} }
} }
@ -282,15 +286,16 @@ mod tests {
use actix_web::web::Data; use actix_web::web::Data;
use awc::ClientBuilder; use awc::ClientBuilder;
use byte_unit::Byte; use byte_unit::Byte;
use std::env;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use tempfile::tempdir;
#[actix_rt::test] #[actix_rt::test]
#[allow(deprecated)] #[allow(deprecated)]
async fn test_paste_data() -> Result<(), Error> { async fn test_paste() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = temp_upload_path.path().to_path_buf();
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
enabled: Some(true), enabled: Some(true),
words: Some(3), words: Some(3),
@ -302,16 +307,20 @@ 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", None, None, &config)?; let file_name = paste.store_file("test.txt", None, None, &config).await?;
assert_eq!("ABC", fs::read_to_string(&file_name)?); let file_path = temp_upload_path.path().join(file_name);
assert_eq!( assert_eq!("ABC", fs::read_to_string(&file_path).await?);
Some("txt"), assert_eq!(Some("txt"), file_path.extension().and_then(|v| v.to_str()));
PathBuf::from(&file_name)
.extension()
.and_then(|v| v.to_str())
);
fs::remove_file(file_name)?;
Ok(())
}
#[actix_rt::test]
#[allow(deprecated)]
async fn test_paste_random() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default();
config.server.upload_path = temp_upload_path.path().to_path_buf();
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
length: Some(4), length: Some(4),
type_: RandomURLType::Alphanumeric, type_: RandomURLType::Alphanumeric,
@ -322,11 +331,11 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115], data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File, type_: PasteType::File,
}; };
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?; let file_name = paste.store_file("foo.tar.gz", None, None, &config).await?;
assert_eq!("tessus", fs::read_to_string(&file_name)?); let file_path = temp_upload_path.path().join(&file_name);
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
assert!(file_name.ends_with(".tar.gz")); assert!(file_name.ends_with(".tar.gz"));
assert!(file_name.starts_with("foo.")); assert!(file_name.starts_with("foo."));
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
length: Some(4), length: Some(4),
@ -338,11 +347,11 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115], data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File, type_: PasteType::File,
}; };
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?; let file_name = paste.store_file(".foo.tar.gz", None, None, &config).await?;
assert_eq!("tessus", fs::read_to_string(&file_name)?); let file_path = temp_upload_path.path().join(&file_name);
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
assert!(file_name.ends_with(".tar.gz")); assert!(file_name.ends_with(".tar.gz"));
assert!(file_name.starts_with(".foo.")); assert!(file_name.starts_with(".foo."));
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
length: Some(4), length: Some(4),
@ -354,21 +363,30 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115], data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File, type_: PasteType::File,
}; };
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?; let file_name = paste.store_file("foo.tar.gz", None, None, &config).await?;
assert_eq!("tessus", fs::read_to_string(&file_name)?); let file_path = temp_upload_path.path().join(&file_name);
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
assert!(file_name.ends_with(".tar.gz")); assert!(file_name.ends_with(".tar.gz"));
fs::remove_file(file_name)?;
Ok(())
}
#[actix_rt::test]
#[allow(deprecated)]
async fn test_paste_with_extension() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default();
config.server.upload_path = temp_upload_path.path().to_path_buf();
config.paste.default_extension = String::from("txt"); config.paste.default_extension = String::from("txt");
config.paste.random_url = None; config.paste.random_url = None;
let paste = Paste { let paste = Paste {
data: vec![120, 121, 122], data: vec![120, 121, 122],
type_: PasteType::File, type_: PasteType::File,
}; };
let file_name = paste.store_file(".foo", None, None, &config)?; let file_name = paste.store_file(".foo", None, None, &config).await?;
assert_eq!("xyz", fs::read_to_string(&file_name)?); let file_path = temp_upload_path.path().join(&file_name);
assert_eq!("xyz", fs::read_to_string(&file_path).await?);
assert_eq!(".foo.txt", file_name); assert_eq!(".foo.txt", file_name);
fs::remove_file(file_name)?;
config.paste.default_extension = String::from("bin"); config.paste.default_extension = String::from("bin");
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
@ -380,16 +398,20 @@ 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", None, None, &config)?; let file_name = paste.store_file("random", None, None, &config).await?;
assert_eq!("xyz", fs::read_to_string(&file_name)?); let file_path = temp_upload_path.path().join(&file_name);
assert_eq!( assert_eq!(Some("bin"), file_path.extension().and_then(|v| v.to_str()));
Some("bin"), assert_eq!("xyz", fs::read_to_string(&file_path).await?);
PathBuf::from(&file_name)
.extension()
.and_then(|v| v.to_str())
);
fs::remove_file(file_name)?;
Ok(())
}
#[actix_rt::test]
#[allow(deprecated)]
async fn test_paste_filename_from_header() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default();
config.server.upload_path = temp_upload_path.path().to_path_buf();
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
length: Some(4), length: Some(4),
type_: RandomURLType::Alphanumeric, type_: RandomURLType::Alphanumeric,
@ -400,15 +422,17 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115], data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File, type_: PasteType::File,
}; };
let file_name = paste.store_file( let file_name = paste
"filename.txt", .store_file(
None, "filename.txt",
Some("fn_from_header.txt".to_string()), None,
&config, Some("fn_from_header.txt".to_string()),
)?; &config,
assert_eq!("tessus", fs::read_to_string(&file_name)?); )
.await?;
assert_eq!("fn_from_header.txt", file_name); assert_eq!("fn_from_header.txt", file_name);
fs::remove_file(file_name)?; let file_path = temp_upload_path.path().join(&file_name);
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
length: Some(4), length: Some(4),
@ -420,63 +444,107 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115], data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File, type_: PasteType::File,
}; };
let file_name = paste.store_file( let file_name = paste
"filename.txt", .store_file(
None, "filename.txt",
Some("fn_from_header".to_string()), None,
&config, Some("fn_from_header".to_string()),
)?; &config,
assert_eq!("tessus", fs::read_to_string(&file_name)?); )
.await?;
let file_path = temp_upload_path.path().join(&file_name);
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
assert_eq!("fn_from_header", file_name); assert_eq!("fn_from_header", file_name);
fs::remove_file(file_name)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] { Ok(())
fs::create_dir_all( }
paste_type
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)?;
}
#[actix_rt::test]
#[allow(deprecated)]
async fn test_paste_oneshot() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = tempdir()?.path().to_path_buf();
config.paste.random_url = None; config.paste.random_url = None;
fs::create_dir_all(
PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)
.await?;
let paste = Paste { let paste = Paste {
data: vec![116, 101, 115, 116], data: vec![116, 101, 115, 116],
type_: PasteType::Oneshot, type_: PasteType::Oneshot,
}; };
let expiry_date = util::get_system_time()?.as_millis() + 100; 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 let file_path = PasteType::Oneshot
.get_path(&config.server.upload_path) .get_path(&config.server.upload_path)
.expect("Bad upload path") .expect("Bad upload path")
.join(format!("{file_name}.{expiry_date}")); .join(format!("{file_name}.{expiry_date}"));
assert_eq!("test", fs::read_to_string(&file_path)?); assert_eq!("test", fs::read_to_string(&file_path).await?);
fs::remove_file(file_path)?; fs::remove_file(file_path).await?;
Ok(())
}
#[actix_rt::test]
#[allow(deprecated)]
async fn test_paste_url() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = tempdir()?.path().to_path_buf();
config.paste.random_url = Some(RandomURLConfig { config.paste.random_url = Some(RandomURLConfig {
enabled: Some(true), enabled: Some(true),
..RandomURLConfig::default() ..RandomURLConfig::default()
}); });
fs::create_dir_all(
PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)
.await?;
let url = String::from("https://orhun.dev/"); let url = String::from("https://orhun.dev/");
let paste = Paste { let paste = Paste {
data: url.as_bytes().to_vec(), data: url.as_bytes().to_vec(),
type_: PasteType::Url, 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 let file_path = PasteType::Url
.get_path(&config.server.upload_path) .get_path(&config.server.upload_path)
.expect("Bad upload path") .expect("Bad upload path")
.join(&file_name); .join(&file_name);
assert_eq!(url, fs::read_to_string(&file_path)?); assert_eq!(url, fs::read_to_string(&file_path).await?);
fs::remove_file(file_path)?; fs::remove_file(file_path).await?;
let url = String::from("testurl.com"); let url = String::from("testurl.com");
let paste = Paste { let paste = Paste {
data: url.as_bytes().to_vec(), data: url.as_bytes().to_vec(),
type_: PasteType::Url, type_: PasteType::Url,
}; };
assert!(paste.store_url(None, &config).is_err()); assert!(paste.store_url(None, &config).await.is_err());
Ok(())
}
#[actix_rt::test]
#[allow(deprecated)]
async fn test_paste_remote_url() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = tempdir()?.path().to_path_buf();
config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte"); config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte");
fs::create_dir_all(
PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)
.await?;
let url = String::from("https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg"); let url = String::from("https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg");
let mut paste = Paste { let mut paste = Paste {
data: url.as_bytes().to_vec(), data: url.as_bytes().to_vec(),
@ -487,26 +555,12 @@ mod tests {
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.finish(), .finish(),
); );
let file_name = paste let _ = paste.store_remote_file(None, &client_data, &config).await?;
.store_remote_file(None, &client_data, &RwLock::new(config.clone()))
.await?;
let file_path = PasteType::RemoteFile
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(file_name);
assert_eq!( assert_eq!(
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed", "70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
util::sha256_digest(&*paste.data)? 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)
.expect("Bad upload path"),
)?;
}
Ok(()) Ok(())
} }

View File

@ -18,20 +18,17 @@ use mime::TEXT_PLAIN_UTF_8;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::env; use std::env;
use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::RwLock;
use std::time::Duration; use std::time::Duration;
use tokio::fs;
use tokio::sync::RwLock;
use uts2ts; use uts2ts;
/// Shows the landing page. /// Shows the landing page.
#[get("/")] #[get("/")]
#[allow(deprecated)] #[allow(deprecated)]
async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> { async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let mut config = config let mut config = config.read().await.clone();
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.clone();
let redirect = HttpResponse::Found() let redirect = HttpResponse::Found()
.append_header(("Location", env!("CARGO_PKG_HOMEPAGE"))) .append_header(("Location", env!("CARGO_PKG_HOMEPAGE")))
.finish(); .finish();
@ -53,7 +50,7 @@ async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error>
} }
if let Some(mut landing_page) = config.landing_page { if let Some(mut landing_page) = config.landing_page {
if let Some(file) = landing_page.file { 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 { match landing_page.text {
Some(page) => Ok(HttpResponse::Ok() Some(page) => Ok(HttpResponse::Ok()
@ -86,15 +83,14 @@ async fn serve(
options: Option<web::Query<ServeOptions>>, options: Option<web::Query<ServeOptions>>,
config: web::Data<RwLock<Config>>, config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let config = config let config = config.read().await;
.read() let mut path =
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?).await?;
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
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, PasteType::OneshotUrl] { 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 = 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() if alt_path.exists()
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir()) || path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
{ {
@ -128,21 +124,23 @@ async fn serve(
file, file,
util::get_system_time()?.as_millis() util::get_system_time()?.as_millis()
)), )),
)?; )
.await?;
} }
Ok(response) Ok(response)
} }
PasteType::Url => Ok(HttpResponse::Found() PasteType::Url => Ok(HttpResponse::Found()
.append_header(("Location", fs::read_to_string(&path)?)) .append_header(("Location", fs::read_to_string(&path).await?))
.finish()), .finish()),
PasteType::OneshotUrl => { PasteType::OneshotUrl => {
let resp = HttpResponse::Found() let resp = HttpResponse::Found()
.append_header(("Location", fs::read_to_string(&path)?)) .append_header(("Location", fs::read_to_string(&path).await?))
.finish(); .finish();
fs::rename( fs::rename(
&path, &path,
path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())), path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())),
)?; )
.await?;
Ok(resp) Ok(resp)
} }
} }
@ -155,14 +153,12 @@ async fn delete(
file: web::Path<String>, file: web::Path<String>,
config: web::Data<RwLock<Config>>, config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let config = config let config = config.read().await;
.read() let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?).await?;
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
if !path.is_file() || !path.exists() { if !path.is_file() || !path.exists() {
return Err(error::ErrorNotFound("file is not found or expired :(\n")); 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()), Ok(_) => info!("deleted file: {:?}", file.to_string()),
Err(e) => { Err(e) => {
error!("cannot delete file: {}", e); error!("cannot delete file: {}", e);
@ -176,9 +172,7 @@ async fn delete(
#[get("/version")] #[get("/version")]
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)] #[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> { async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let config = config let config = config.read().await;
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
if !config.server.expose_version.unwrap_or(false) { if !config.server.expose_version.unwrap_or(false) {
warn!("server is not configured to expose version endpoint"); warn!("server is not configured to expose version endpoint");
Err(error::ErrorNotFound(""))?; Err(error::ErrorNotFound(""))?;
@ -199,13 +193,8 @@ async fn upload(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let connection = request.connection_info().clone(); let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host"); let host = connection.realip_remote_addr().unwrap_or("unknown host");
let server_url = match config let config = config.read().await;
.read() let server_url = match config.server.url.clone() {
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.server
.url
.clone()
{
Some(v) => v, Some(v) => v,
None => { None => {
format!("{}://{}", connection.scheme(), connection.host(),) format!("{}://{}", connection.scheme(), connection.host(),)
@ -215,8 +204,6 @@ async fn upload(
let mut expiry_date = header::parse_expiry_date(request.headers(), time)?; let mut expiry_date = header::parse_expiry_date(request.headers(), time)?;
if expiry_date.is_none() { if expiry_date.is_none() {
expiry_date = config expiry_date = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.paste .paste
.default_expiry .default_expiry
.and_then(|v| time.checked_add(v).map(|t| t.as_millis())); .and_then(|v| time.checked_add(v).map(|t| t.as_millis()));
@ -239,18 +226,11 @@ async fn upload(
&& paste_type != PasteType::RemoteFile && paste_type != PasteType::RemoteFile
&& paste_type != PasteType::OneshotUrl && paste_type != PasteType::OneshotUrl
&& expiry_date.is_none() && expiry_date.is_none()
&& !config && !config.paste.duplicate_files.unwrap_or(true)
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.paste
.duplicate_files
.unwrap_or(true)
{ {
let bytes_checksum = util::sha256_digest(&*bytes)?; let bytes_checksum = util::sha256_digest(&*bytes)?;
let config = config if let Some(file) = Directory::try_from(config.server.upload_path.as_path())
.read() .map_err(error::ErrorInternalServerError)?
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
if let Some(file) = Directory::try_from(config.server.upload_path.as_path())?
.get_file(bytes_checksum) .get_file(bytes_checksum)
{ {
urls.push(format!( urls.push(format!(
@ -270,15 +250,14 @@ async fn upload(
}; };
let mut file_name = match paste.type_ { let mut file_name = match paste.type_ {
PasteType::File | PasteType::Oneshot => { PasteType::File | PasteType::Oneshot => {
let config = config paste
.read() .store_file(
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; content.get_file_name()?,
paste.store_file( expiry_date,
content.get_file_name()?, header_filename,
expiry_date, &config,
header_filename, )
&config, .await?
)?
} }
PasteType::RemoteFile => { PasteType::RemoteFile => {
paste paste
@ -286,10 +265,7 @@ async fn upload(
.await? .await?
} }
PasteType::Url | PasteType::OneshotUrl => { PasteType::Url | PasteType::OneshotUrl => {
let config = config paste.store_url(expiry_date, &config).await?
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
paste.store_url(expiry_date, &config)?
} }
}; };
info!( info!(
@ -300,9 +276,6 @@ async fn upload(
.get_appropriate_unit(UnitType::Decimal), .get_appropriate_unit(UnitType::Decimal),
host host
); );
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
if let Some(handle_spaces_config) = config.server.handle_spaces { if let Some(handle_spaces_config) = config.server.handle_spaces {
file_name = handle_spaces_config.process_filename(&file_name); file_name = handle_spaces_config.process_filename(&file_name);
} }
@ -330,53 +303,58 @@ pub struct ListItem {
#[get("/list")] #[get("/list")]
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)] #[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> { async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let config = config let config = config.read().await;
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.clone();
if !config.server.expose_list.unwrap_or(false) { if !config.server.expose_list.unwrap_or(false) {
warn!("server is not configured to expose list endpoint"); 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| { let mut entries: Vec<ListItem> = Vec::new();
entry.ok().and_then(|e| {
let metadata = match e.metadata() { let mut dir_contents = fs::read_dir(&config.server.upload_path).await?;
Ok(metadata) => {
if metadata.is_dir() { let system_time = util::get_system_time()?;
return None;
} while let Ok(Some(entry)) = dir_contents.next_entry().await {
metadata let metadata = match entry.metadata().await {
} Ok(metadata) if metadata.is_dir() => continue,
Err(e) => { Ok(metadata) => metadata,
error!("failed to read metadata: {e}"); Err(e) => {
return None; error!("failed to read metadata: {e}");
} continue;
}; }
let mut file_name = PathBuf::from(e.file_name()); };
let expires_at_utc = if let Some(expiration) = file_name
.extension() let mut file_name = PathBuf::from(entry.file_name());
.and_then(|ext| ext.to_str())
.and_then(|v| v.parse::<i64>().ok()) let expires_at_utc = match file_name
{ .extension()
file_name.set_extension(""); .and_then(|ext| ext.to_str())
if util::get_system_time().ok()? .and_then(|v| v.parse::<u64>().ok())
> Duration::from_millis(expiration.try_into().ok()?) {
{ Some(expiration) if system_time > Duration::from_millis(expiration) => continue,
return None; Some(expiration) => {
} file_name.set_extension("");
Some(uts2ts::uts2ts(expiration / 1000).as_string()) Some(
} else { uts2ts::uts2ts(
None (expiration / 1000)
}; .try_into()
Some(ListItem { .map_err(|_| error::ErrorInternalServerError("Invalid timestamp"))?,
file_name, )
file_size: metadata.len(), .as_string(),
expires_at_utc, )
}) }
}) None => None,
}) };
.collect();
entries.push(ListItem {
file_name,
file_size: metadata.len(),
expires_at_utc,
});
}
Ok(HttpResponse::Ok().json(entries)) Ok(HttpResponse::Ok().json(entries))
} }
@ -420,6 +398,7 @@ mod tests {
use std::str; use std::str;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use tempfile::tempdir;
fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest { fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest {
let multipart_data = format!( let multipart_data = format!(
@ -523,7 +502,7 @@ mod tests {
let response = test::call_service(&app, request).await; let response = test::call_service(&app, request).await;
assert_eq!(StatusCode::OK, response.status()); assert_eq!(StatusCode::OK, response.status());
assert_body(response.into_body(), "landing page from file").await?; assert_body(response.into_body(), "landing page from file").await?;
fs::remove_file(filename)?; fs::remove_file(filename).await?;
Ok(()) Ok(())
} }
@ -626,7 +605,7 @@ mod tests {
config.server.expose_list = Some(true); config.server.expose_list = Some(true);
let test_upload_dir = "test_upload"; 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); config.server.upload_path = PathBuf::from(test_upload_dir);
let app = test::init_service( let app = test::init_service(
@ -657,7 +636,7 @@ mod tests {
PathBuf::from(filename) PathBuf::from(filename)
); );
fs::remove_dir_all(test_upload_dir)?; fs::remove_dir_all(test_upload_dir).await?;
Ok(()) Ok(())
} }
@ -668,7 +647,7 @@ mod tests {
config.server.expose_list = Some(true); config.server.expose_list = Some(true);
let test_upload_dir = "test_upload"; 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); config.server.upload_path = PathBuf::from(test_upload_dir);
let app = test::init_service( let app = test::init_service(
@ -702,7 +681,7 @@ mod tests {
assert!(result.is_empty()); assert!(result.is_empty());
fs::remove_dir_all(test_upload_dir)?; fs::remove_dir_all(test_upload_dir).await?;
Ok(()) Ok(())
} }
@ -752,9 +731,10 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_delete_file() -> Result<(), Error> { async fn test_delete_file() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.delete_tokens = Some(["test".to_string()].into()); config.server.delete_tokens = Some(["test".to_string()].into());
config.server.upload_path = env::current_dir()?; config.server.upload_path = temp_upload_path.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -781,8 +761,7 @@ mod tests {
assert_eq!(StatusCode::OK, response.status()); assert_eq!(StatusCode::OK, response.status());
assert_body(response.into_body(), "file deleted\n").await?; assert_body(response.into_body(), "file deleted\n").await?;
let path = PathBuf::from(file_name); assert!(!temp_upload_path.path().join(file_name).exists());
assert!(!path.exists());
Ok(()) Ok(())
} }
@ -790,7 +769,7 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_delete_file_without_token_in_config() -> Result<(), Error> { async fn test_delete_file_without_token_in_config() -> Result<(), Error> {
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = tempdir()?.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -815,8 +794,9 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_file() -> Result<(), Error> { async fn test_upload_file() -> Result<(), Error> {
let test_delete_file = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = test_delete_file.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -847,7 +827,7 @@ mod tests {
assert_eq!(StatusCode::OK, response.status()); assert_eq!(StatusCode::OK, response.status());
assert_body(response.into_body(), &timestamp).await?; assert_body(response.into_body(), &timestamp).await?;
fs::remove_file(file_name)?; fs::remove_file(test_delete_file.path().join(file_name)).await?;
let serve_request = TestRequest::get() let serve_request = TestRequest::get()
.uri(&format!("/{file_name}")) .uri(&format!("/{file_name}"))
.to_request(); .to_request();
@ -859,8 +839,9 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_file_override_filename() -> Result<(), Error> { async fn test_upload_file_override_filename() -> Result<(), Error> {
let test_delete_file = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = test_delete_file.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -897,7 +878,7 @@ mod tests {
assert_eq!(StatusCode::OK, response.status()); assert_eq!(StatusCode::OK, response.status());
assert_body(response.into_body(), &timestamp).await?; assert_body(response.into_body(), &timestamp).await?;
fs::remove_file(header_filename)?; fs::remove_file(test_delete_file.path().join(header_filename)).await?;
let serve_request = TestRequest::get() let serve_request = TestRequest::get()
.uri(&format!("/{header_filename}")) .uri(&format!("/{header_filename}"))
.to_request(); .to_request();
@ -909,8 +890,9 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_same_filename() -> Result<(), Error> { async fn test_upload_same_filename() -> Result<(), Error> {
let temp_upload_dir = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = temp_upload_dir.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -954,7 +936,7 @@ mod tests {
assert_eq!(StatusCode::CONFLICT, response.status()); assert_eq!(StatusCode::CONFLICT, response.status());
assert_body(response.into_body(), "file already exists\n").await?; assert_body(response.into_body(), "file already exists\n").await?;
fs::remove_file(header_filename)?; fs::remove_file(temp_upload_dir.path().join(header_filename)).await?;
Ok(()) Ok(())
} }
@ -963,7 +945,7 @@ mod tests {
#[allow(deprecated)] #[allow(deprecated)]
async fn test_upload_duplicate_file() -> Result<(), Error> { async fn test_upload_duplicate_file() -> Result<(), Error> {
let test_upload_dir = "test_upload"; let test_upload_dir = "test_upload";
fs::create_dir(test_upload_dir)?; fs::create_dir(test_upload_dir).await?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = PathBuf::from(&test_upload_dir); config.server.upload_path = PathBuf::from(&test_upload_dir);
@ -1002,15 +984,16 @@ mod tests {
assert_eq!(first_body_bytes, second_body_bytes); assert_eq!(first_body_bytes, second_body_bytes);
fs::remove_dir_all(test_upload_dir)?; fs::remove_dir_all(test_upload_dir).await?;
Ok(()) Ok(())
} }
#[actix_web::test] #[actix_web::test]
async fn test_upload_expiring_file() -> Result<(), Error> { async fn test_upload_expiring_file() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = temp_upload_path.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -1054,11 +1037,16 @@ mod tests {
let response = test::call_service(&app, serve_request).await; let response = test::call_service(&app, serve_request).await;
assert_eq!(StatusCode::NOT_FOUND, response.status()); assert_eq!(StatusCode::NOT_FOUND, response.status());
if let Some(glob_path) = glob(&format!("{file_name}.[0-9]*")) if let Some(glob_path) = glob(
.map_err(error::ErrorInternalServerError)? &temp_upload_path
.next() .path()
.join(format!("{file_name}.[0-9]*"))
.to_string_lossy(),
)
.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(()) Ok(())
@ -1066,8 +1054,9 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_remote_file() -> Result<(), Error> { async fn test_upload_remote_file() -> Result<(), Error> {
let temp_upload_dir = tempdir()?;
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = temp_upload_dir.path().to_path_buf();
config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default(); config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default();
let app = test::init_service( let app = test::init_service(
@ -1113,7 +1102,7 @@ mod tests {
util::sha256_digest(&*body_bytes)? util::sha256_digest(&*body_bytes)?
); );
fs::remove_file(file_name)?; fs::remove_file(temp_upload_dir.path().join(file_name)).await?;
let serve_request = TestRequest::get() let serve_request = TestRequest::get()
.uri(&format!("/{file_name}")) .uri(&format!("/{file_name}"))
@ -1127,7 +1116,7 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_url() -> Result<(), Error> { async fn test_upload_url() -> Result<(), Error> {
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = tempdir()?.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -1140,7 +1129,7 @@ mod tests {
let url_upload_path = PasteType::Url let url_upload_path = PasteType::Url
.get_path(&config.server.upload_path) .get_path(&config.server.upload_path)
.expect("Bad 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( let response = test::call_service(
&app, &app,
@ -1154,8 +1143,8 @@ mod tests {
let response = test::call_service(&app, serve_request).await; let response = test::call_service(&app, serve_request).await;
assert_eq!(StatusCode::FOUND, response.status()); assert_eq!(StatusCode::FOUND, response.status());
fs::remove_file(url_upload_path.join("url"))?; fs::remove_file(url_upload_path.join("url")).await?;
fs::remove_dir(url_upload_path)?; fs::remove_dir(url_upload_path).await?;
let serve_request = TestRequest::get().uri("/url").to_request(); let serve_request = TestRequest::get().uri("/url").to_request();
let response = test::call_service(&app, serve_request).await; let response = test::call_service(&app, serve_request).await;
@ -1167,7 +1156,7 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_oneshot() -> Result<(), Error> { async fn test_upload_oneshot() -> Result<(), Error> {
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = tempdir()?.path().to_path_buf();
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -1180,7 +1169,7 @@ mod tests {
let oneshot_upload_path = PasteType::Oneshot let oneshot_upload_path = PasteType::Oneshot
.get_path(&config.server.upload_path) .get_path(&config.server.upload_path)
.expect("Bad 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 file_name = "oneshot.txt";
let timestamp = util::get_system_time()?.as_secs().to_string(); let timestamp = util::get_system_time()?.as_secs().to_string();
@ -1217,9 +1206,9 @@ mod tests {
.map_err(error::ErrorInternalServerError)? .map_err(error::ErrorInternalServerError)?
.next() .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(()) Ok(())
} }
@ -1227,7 +1216,7 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_upload_oneshot_url() -> Result<(), Error> { async fn test_upload_oneshot_url() -> Result<(), Error> {
let mut config = Config::default(); let mut config = Config::default();
config.server.upload_path = env::current_dir()?; config.server.upload_path = tempdir()?.path().to_path_buf();
let oneshot_url_suffix = "oneshot_url"; let oneshot_url_suffix = "oneshot_url";
@ -1242,7 +1231,7 @@ mod tests {
let url_upload_path = PasteType::OneshotUrl let url_upload_path = PasteType::OneshotUrl
.get_path(&config.server.upload_path) .get_path(&config.server.upload_path)
.expect("Bad 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( let response = test::call_service(
&app, &app,
@ -1272,7 +1261,7 @@ mod tests {
assert_eq!(StatusCode::NOT_FOUND, response.status()); assert_eq!(StatusCode::NOT_FOUND, response.status());
// Cleanup // Cleanup
fs::remove_dir_all(url_upload_path)?; fs::remove_dir_all(url_upload_path).await?;
Ok(()) 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::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tokio::task::spawn_blocking;
/// Regex for matching the timestamp extension of a path. /// Regex for matching the timestamp extension of a path.
pub static TIMESTAMP_EXTENSION_REGEX: Lazy<Regex> = lazy_regex!(r#"\.[0-9]{10,}$"#); 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. /// 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. /// 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( path = PathBuf::from(
TIMESTAMP_EXTENSION_REGEX TIMESTAMP_EXTENSION_REGEX
.replacen( .replacen(
@ -36,10 +37,15 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
) )
.to_string(), .to_string(),
); );
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
.map_err(error::ErrorInternalServerError)? let path_string = path.to_string_lossy().into_owned();
.last() 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)?; let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
if let Some(extension) = glob_path if let Some(extension) = glob_path
.extension() .extension()
@ -137,6 +143,8 @@ mod tests {
use std::env; use std::env;
use std::fs; use std::fs;
use std::thread; use std::thread;
use tempfile::tempdir;
#[test] #[test]
fn test_system_time() -> Result<(), ActixError> { fn test_system_time() -> Result<(), ActixError> {
let system_time = get_system_time()?.as_millis(); let system_time = get_system_time()?.as_millis();
@ -145,19 +153,19 @@ mod tests {
Ok(()) Ok(())
} }
#[test] #[actix_rt::test]
fn test_glob_match() -> Result<(), ActixError> { async fn test_glob_match() -> Result<(), ActixError> {
let path = PathBuf::from(format!( let path = PathBuf::from(format!(
"expired.file1.{}", "expired.file1.{}",
get_system_time()?.as_millis() + 50 get_system_time()?.as_millis() + 50
)); ));
fs::write(&path, String::new())?; 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)); thread::sleep(Duration::from_millis(75));
assert_eq!( assert_eq!(
PathBuf::from("expired.file1"), PathBuf::from("expired.file1"),
glob_match_file(PathBuf::from("expired.file1"))? glob_match_file(PathBuf::from("expired.file1")).await?
); );
fs::remove_file(path)?; fs::remove_file(path)?;
@ -179,18 +187,16 @@ mod tests {
#[test] #[test]
fn test_get_expired_files() -> Result<(), ActixError> { fn test_get_expired_files() -> Result<(), ActixError> {
let current_dir = env::current_dir()?; let test_temp_dir = tempdir()?;
let test_dir = test_temp_dir.path();
let expiration_time = get_system_time()?.as_millis() + 50; let expiration_time = get_system_time()?.as_millis() + 50;
let path = PathBuf::from(format!("expired.file2.{expiration_time}")); let path = test_dir.join(format!("expired.file2.{expiration_time}"));
fs::write(&path, String::new())?; fs::write(&path, String::new())?;
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(&current_dir)); assert_eq!(Vec::<PathBuf>::new(), get_expired_files(test_dir));
thread::sleep(Duration::from_millis(75)); thread::sleep(Duration::from_millis(75));
assert_eq!( assert_eq!(vec![path.clone()], get_expired_files(test_dir));
vec![current_dir.join(&path)], fs::remove_file(&path)?;
get_expired_files(&current_dir) assert_eq!(Vec::<PathBuf>::new(), get_expired_files(test_dir));
);
fs::remove_file(path)?;
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(&current_dir));
Ok(()) Ok(())
} }