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:
OUT_DIR: target
- name: Upload reports to codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
name: code-coverage-report
file: lcov.info
flags: unit-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
fixtures:
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/),
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
### Removed

1027
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -5,7 +5,7 @@ use actix_web::http::Method;
use actix_web::middleware::ErrorHandlerResponse;
use actix_web::{error, web, Error};
use std::collections::HashSet;
use std::sync::RwLock;
use tokio::sync::RwLock;
/// 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
.app_data::<web::Data<RwLock<Config>>>()
.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);

View File

@ -120,13 +120,20 @@ pub struct PasteConfig {
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.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CleanupConfig {
/// Enable cleaning up.
pub enabled: bool,
/// Interval between clean-ups.
#[serde(default, with = "humantime_serde")]
#[serde(default = "get_default_cleanup_interval", with = "humantime_serde")]
pub interval: Duration,
}

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,7 +5,7 @@ use actix_web::{App, HttpServer};
use awc::ClientBuilder;
use hotwatch::notify::event::ModifyKind;
use hotwatch::{Event, EventKind, Hotwatch};
use rustypaste::config::{Config, ServerConfig};
use rustypaste::config::{Config, DEFAULT_CLEANUP_INTERVAL};
use rustypaste::middleware::ContentLengthLimiter;
use rustypaste::paste::PasteType;
use rustypaste::server;
@ -15,9 +15,9 @@ use std::env;
use std::fs;
use std::io::Result as IoResult;
use std::path::{Path, PathBuf};
use std::sync::{mpsc, RwLock};
use std::thread;
use std::time::Duration;
use tokio::sync::RwLock;
#[cfg(not(feature = "shuttle"))]
use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter,
@ -38,7 +38,7 @@ extern crate tracing;
/// * initializes the logger
/// * creates the necessary directories
/// * 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.
dotenvy::dotenv().ok();
@ -61,6 +61,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
}
None => config_folder.join("config.toml"),
};
if !config_path.exists() {
error!(
"{} 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);
}
let config = Config::parse(&config_path).expect("failed to parse config");
trace!("{:#?}", config);
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.
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] {
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.
@ -91,82 +90,71 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
)
.expect("failed to initialize configuration file watcher");
let config_lock = Data::new(RwLock::new(config));
// Hot-reload the configuration file.
let config = Data::new(RwLock::new(config));
let cloned_config = Data::clone(&config);
let config_watcher_config = config_lock.clone();
let config_watcher = move |event: Event| {
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
(event.kind, event.paths.first())
{
info!("Reloading configuration");
match Config::parse(path) {
Ok(config) => match cloned_config.write() {
Ok(mut cloned_config) => {
*cloned_config = config.clone();
info!("Configuration has been updated.");
if let Err(e) = config_sender.send(config) {
error!("Failed to send config for the cleanup routine: {}", e)
}
cloned_config.warn_deprecation();
}
Err(e) => {
error!("Failed to acquire config: {}", e);
}
},
Ok(new_config) => {
let mut locked_config = config_watcher_config.blocking_write();
*locked_config = new_config;
info!("Configuration has been updated.");
locked_config.warn_deprecation();
}
Err(e) => {
error!("Failed to update config: {}", e);
}
}
}
};
hotwatch
.watch(&config_path, config_watcher)
.unwrap_or_else(|_| panic!("failed to watch {config_path:?}"));
// 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 {
let mut enabled = false;
if let Some(ref cleanup_config) = paste_config
.read()
.ok()
.and_then(|v| v.delete_expired_files.clone())
// Additional context block to ensure the config lock is dropped
{
if cleanup_config.enabled {
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),
let locked_config = expired_files_config.blocking_read();
let upload_path = locked_config.server.upload_path.clone();
if let Some(ref cleanup_config) = locked_config.paste.delete_expired_files {
if cleanup_config.enabled {
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),
}
}
}
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);
cleanup_interval = cleanup_config.interval;
}
}
}
thread::sleep(cleanup_interval);
});
Ok((config, server_config, hotwatch))
Ok((config_lock, hotwatch))
}
#[cfg(not(feature = "shuttle"))]
#[actix_web::main]
async fn main() -> IoResult<()> {
// 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.
let mut http_server = HttpServer::new(move || {
@ -203,7 +191,10 @@ async fn main() -> IoResult<()> {
#[shuttle_runtime::main]
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
// 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.
let service_config = move |cfg: &mut ServiceConfig| {

View File

@ -5,11 +5,12 @@ 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 +93,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>,
@ -176,6 +177,7 @@ impl Paste {
.unwrap_or_default()
.to_string();
let file_path = util::glob_match_file(path.clone())
.await
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
if file_path.is_file() && file_path.exists() {
return Err(error::ErrorConflict("file already exists\n"));
@ -183,8 +185,8 @@ impl Paste {
if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}"));
}
let mut buffer = File::create(&path)?;
buffer.write_all(&self.data)?;
let mut buffer = File::create(&path).await?;
buffer.write_all(&self.data).await?;
Ok(file_name)
}
@ -200,7 +202,7 @@ impl Paste {
&mut self,
expiry_date: Option<u128>,
client: &Client,
config: &RwLock<Config>,
config: &Config,
) -> Result<String, Error> {
let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?;
let url = Url::parse(data).map_err(error::ErrorBadRequest)?;
@ -215,8 +217,6 @@ impl Paste {
.await
.map_err(error::ErrorInternalServerError)?;
let payload_limit = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.server
.max_content_length
.try_into()
@ -227,15 +227,19 @@ impl Paste {
.await
.map_err(error::ErrorInternalServerError)?
.to_vec();
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let bytes_checksum = util::sha256_digest(&*bytes)?;
self.data = bytes;
if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() {
if let Some(file) =
Directory::try_from(config.server.upload_path.as_path())?.get_file(bytes_checksum)
{
let upload_path = config.server.upload_path.clone();
let directory =
match spawn_blocking(move || Directory::try_from(upload_path.as_path())).await {
Ok(Ok(d)) => d,
Ok(Err(e)) => return Err(error::ErrorInternalServerError(e)),
Err(e) => return Err(error::ErrorInternalServerError(e)),
};
if let Some(file) = directory.get_file(bytes_checksum) {
return Ok(file
.path
.file_name()
@ -244,7 +248,7 @@ impl Paste {
.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.
@ -254,7 +258,7 @@ impl Paste {
///
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
#[allow(deprecated)]
pub fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
pub async fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
let data = str::from_utf8(&self.data)
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
let url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
@ -269,7 +273,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)
}
}
@ -282,15 +286,16 @@ mod tests {
use actix_web::web::Data;
use awc::ClientBuilder;
use byte_unit::Byte;
use std::env;
use std::str::FromStr;
use std::time::Duration;
use tempfile::tempdir;
#[actix_rt::test]
#[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();
config.server.upload_path = env::current_dir()?;
config.server.upload_path = temp_upload_path.path().to_path_buf();
config.paste.random_url = Some(RandomURLConfig {
enabled: Some(true),
words: Some(3),
@ -302,16 +307,20 @@ 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)?);
assert_eq!(
Some("txt"),
PathBuf::from(&file_name)
.extension()
.and_then(|v| v.to_str())
);
fs::remove_file(file_name)?;
let file_name = paste.store_file("test.txt", None, None, &config).await?;
let file_path = temp_upload_path.path().join(file_name);
assert_eq!("ABC", fs::read_to_string(&file_path).await?);
assert_eq!(Some("txt"), file_path.extension().and_then(|v| v.to_str()));
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 {
length: Some(4),
type_: RandomURLType::Alphanumeric,
@ -322,11 +331,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?;
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.starts_with("foo."));
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig {
length: Some(4),
@ -338,11 +347,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?;
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.starts_with(".foo."));
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig {
length: Some(4),
@ -354,21 +363,30 @@ 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?;
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"));
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.random_url = None;
let paste = Paste {
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?;
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);
fs::remove_file(file_name)?;
config.paste.default_extension = String::from("bin");
config.paste.random_url = Some(RandomURLConfig {
@ -380,16 +398,20 @@ 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)?);
assert_eq!(
Some("bin"),
PathBuf::from(&file_name)
.extension()
.and_then(|v| v.to_str())
);
fs::remove_file(file_name)?;
let file_name = paste.store_file("random", None, None, &config).await?;
let file_path = temp_upload_path.path().join(&file_name);
assert_eq!(Some("bin"), file_path.extension().and_then(|v| v.to_str()));
assert_eq!("xyz", fs::read_to_string(&file_path).await?);
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 {
length: Some(4),
type_: RandomURLType::Alphanumeric,
@ -400,15 +422,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!("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 {
length: Some(4),
@ -420,63 +444,107 @@ 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?;
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);
fs::remove_file(file_name)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
fs::create_dir_all(
paste_type
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)?;
}
Ok(())
}
#[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;
fs::create_dir_all(
PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)
.await?;
let paste = Paste {
data: vec![116, 101, 115, 116],
type_: PasteType::Oneshot,
};
let expiry_date = util::get_system_time()?.as_millis() + 100;
let file_name = paste.store_file("test.file", Some(expiry_date), 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?;
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 {
enabled: Some(true),
..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 paste = Paste {
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());
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");
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 mut paste = Paste {
data: url.as_bytes().to_vec(),
@ -487,26 +555,12 @@ mod tests {
.timeout(Duration::from_secs(30))
.finish(),
);
let file_name = paste
.store_remote_file(None, &client_data, &RwLock::new(config.clone()))
.await?;
let file_path = PasteType::RemoteFile
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(file_name);
let _ = paste.store_remote_file(None, &client_data, &config).await?;
assert_eq!(
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
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(())
}

View File

@ -18,20 +18,17 @@ 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 tokio::sync::RwLock;
use uts2ts;
/// Shows the landing page.
#[get("/")]
#[allow(deprecated)]
async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let mut config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.clone();
let mut config = config.read().await.clone();
let redirect = HttpResponse::Found()
.append_header(("Location", env!("CARGO_PKG_HOMEPAGE")))
.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(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()
@ -86,15 +83,14 @@ async fn serve(
options: Option<web::Query<ServeOptions>>,
config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> {
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 config = config.read().await;
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 +124,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)
}
}
@ -155,14 +153,12 @@ async fn delete(
file: web::Path<String>,
config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> {
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 config = config.read().await;
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);
@ -176,9 +172,7 @@ async fn delete(
#[get("/version")]
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let config = config.read().await;
if !config.server.expose_version.unwrap_or(false) {
warn!("server is not configured to expose version endpoint");
Err(error::ErrorNotFound(""))?;
@ -199,13 +193,8 @@ async fn upload(
) -> Result<HttpResponse, Error> {
let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");
let server_url = match config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.server
.url
.clone()
{
let config = config.read().await;
let server_url = match config.server.url.clone() {
Some(v) => v,
None => {
format!("{}://{}", connection.scheme(), connection.host(),)
@ -215,8 +204,6 @@ async fn upload(
let mut expiry_date = header::parse_expiry_date(request.headers(), time)?;
if expiry_date.is_none() {
expiry_date = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.paste
.default_expiry
.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::OneshotUrl
&& expiry_date.is_none()
&& !config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.paste
.duplicate_files
.unwrap_or(true)
&& !config.paste.duplicate_files.unwrap_or(true)
{
let bytes_checksum = util::sha256_digest(&*bytes)?;
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!(
@ -270,15 +250,14 @@ async fn upload(
};
let mut file_name = match paste.type_ {
PasteType::File | PasteType::Oneshot => {
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
@ -286,10 +265,7 @@ async fn upload(
.await?
}
PasteType::Url | PasteType::OneshotUrl => {
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!(
@ -300,9 +276,6 @@ async fn upload(
.get_appropriate_unit(UnitType::Decimal),
host
);
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
if let Some(handle_spaces_config) = config.server.handle_spaces {
file_name = handle_spaces_config.process_filename(&file_name);
}
@ -330,53 +303,58 @@ pub struct ListItem {
#[get("/list")]
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.clone();
let config = config.read().await;
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))
}
@ -420,6 +398,7 @@ mod tests {
use std::str;
use std::thread;
use std::time::Duration;
use tempfile::tempdir;
fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest {
let multipart_data = format!(
@ -523,7 +502,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 +605,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 +636,7 @@ mod tests {
PathBuf::from(filename)
);
fs::remove_dir_all(test_upload_dir)?;
fs::remove_dir_all(test_upload_dir).await?;
Ok(())
}
@ -668,7 +647,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 +681,7 @@ mod tests {
assert!(result.is_empty());
fs::remove_dir_all(test_upload_dir)?;
fs::remove_dir_all(test_upload_dir).await?;
Ok(())
}
@ -752,9 +731,10 @@ mod tests {
#[actix_web::test]
async fn test_delete_file() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
let mut config = Config::default();
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(
App::new()
@ -781,8 +761,7 @@ mod tests {
assert_eq!(StatusCode::OK, response.status());
assert_body(response.into_body(), "file deleted\n").await?;
let path = PathBuf::from(file_name);
assert!(!path.exists());
assert!(!temp_upload_path.path().join(file_name).exists());
Ok(())
}
@ -790,7 +769,7 @@ mod tests {
#[actix_web::test]
async fn test_delete_file_without_token_in_config() -> Result<(), Error> {
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(
App::new()
@ -815,8 +794,9 @@ mod tests {
#[actix_web::test]
async fn test_upload_file() -> Result<(), Error> {
let test_delete_file = tempdir()?;
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(
App::new()
@ -847,7 +827,7 @@ mod tests {
assert_eq!(StatusCode::OK, response.status());
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()
.uri(&format!("/{file_name}"))
.to_request();
@ -859,8 +839,9 @@ mod tests {
#[actix_web::test]
async fn test_upload_file_override_filename() -> Result<(), Error> {
let test_delete_file = tempdir()?;
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(
App::new()
@ -897,7 +878,7 @@ mod tests {
assert_eq!(StatusCode::OK, response.status());
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()
.uri(&format!("/{header_filename}"))
.to_request();
@ -909,8 +890,9 @@ mod tests {
#[actix_web::test]
async fn test_upload_same_filename() -> Result<(), Error> {
let temp_upload_dir = tempdir()?;
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(
App::new()
@ -954,7 +936,7 @@ mod tests {
assert_eq!(StatusCode::CONFLICT, response.status());
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(())
}
@ -963,7 +945,7 @@ mod tests {
#[allow(deprecated)]
async fn test_upload_duplicate_file() -> Result<(), Error> {
let test_upload_dir = "test_upload";
fs::create_dir(test_upload_dir)?;
fs::create_dir(test_upload_dir).await?;
let mut config = Config::default();
config.server.upload_path = PathBuf::from(&test_upload_dir);
@ -1002,15 +984,16 @@ 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(())
}
#[actix_web::test]
async fn test_upload_expiring_file() -> Result<(), Error> {
let temp_upload_path = tempdir()?;
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(
App::new()
@ -1054,11 +1037,16 @@ mod tests {
let response = test::call_service(&app, serve_request).await;
assert_eq!(StatusCode::NOT_FOUND, response.status());
if let Some(glob_path) = glob(&format!("{file_name}.[0-9]*"))
.map_err(error::ErrorInternalServerError)?
.next()
if let Some(glob_path) = glob(
&temp_upload_path
.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(())
@ -1066,8 +1054,9 @@ mod tests {
#[actix_web::test]
async fn test_upload_remote_file() -> Result<(), Error> {
let temp_upload_dir = tempdir()?;
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();
let app = test::init_service(
@ -1113,7 +1102,7 @@ mod tests {
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()
.uri(&format!("/{file_name}"))
@ -1127,7 +1116,7 @@ mod tests {
#[actix_web::test]
async fn test_upload_url() -> Result<(), Error> {
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(
App::new()
@ -1140,7 +1129,7 @@ mod tests {
let url_upload_path = PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&url_upload_path)?;
fs::create_dir_all(&url_upload_path).await?;
let response = test::call_service(
&app,
@ -1154,8 +1143,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;
@ -1167,7 +1156,7 @@ mod tests {
#[actix_web::test]
async fn test_upload_oneshot() -> Result<(), Error> {
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(
App::new()
@ -1180,7 +1169,7 @@ mod tests {
let oneshot_upload_path = PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&oneshot_upload_path)?;
fs::create_dir_all(&oneshot_upload_path).await?;
let file_name = "oneshot.txt";
let timestamp = util::get_system_time()?.as_secs().to_string();
@ -1217,9 +1206,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(())
}
@ -1227,7 +1216,7 @@ mod tests {
#[actix_web::test]
async fn test_upload_oneshot_url() -> Result<(), Error> {
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";
@ -1242,7 +1231,7 @@ mod tests {
let url_upload_path = PasteType::OneshotUrl
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&url_upload_path)?;
fs::create_dir_all(&url_upload_path).await?;
let response = test::call_service(
&app,
@ -1272,7 +1261,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()
@ -137,6 +143,8 @@ mod tests {
use std::env;
use std::fs;
use std::thread;
use tempfile::tempdir;
#[test]
fn test_system_time() -> Result<(), ActixError> {
let system_time = get_system_time()?.as_millis();
@ -145,19 +153,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)?;
@ -179,18 +187,16 @@ mod tests {
#[test]
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 path = PathBuf::from(format!("expired.file2.{expiration_time}"));
let path = test_dir.join(format!("expired.file2.{expiration_time}"));
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));
assert_eq!(
vec![current_dir.join(&path)],
get_expired_files(&current_dir)
);
fs::remove_file(path)?;
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(&current_dir));
assert_eq!(vec![path.clone()], get_expired_files(test_dir));
fs::remove_file(&path)?;
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(test_dir));
Ok(())
}