Compare commits
21 Commits
b4fd2e5666
...
2a25c78976
Author | SHA1 | Date |
---|---|---|
Jake Howard | 2a25c78976 | |
Orhun Parmaksız | 3fccfd61c8 | |
dependabot[bot] | 15e3619479 | |
Orhun Parmaksız | 2037530078 | |
Orhun Parmaksız | 44c07a3eb6 | |
Orhun Parmaksız | 64d783bd0d | |
Orhun Parmaksız | 1fd561f869 | |
dependabot[bot] | c6d6da6296 | |
Orhun Parmaksız | 54e2ddc91a | |
dependabot[bot] | 8e505c0da8 | |
Orhun Parmaksız | 0f0ba72305 | |
dependabot[bot] | 77e97573ef | |
Jake Howard | 18a1abf771 | |
Orhun Parmaksız | 49eadb63d5 | |
Jake Howard | dd2a635574 | |
Jake Howard | 46210d22f3 | |
Orhun Parmaksız | 8cd16369f0 | |
Jake Howard | 3547c079e7 | |
Jake Howard | fc9e493408 | |
Jake Howard | 07fb759b5d | |
Jake Howard | 6a43cbd620 |
|
@ -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:
|
||||
|
|
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
17
src/file.rs
17
src/file.rs
|
@ -1,5 +1,4 @@
|
|||
use crate::util;
|
||||
use actix_web::{error, Error as ActixError};
|
||||
use glob::glob;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File as OsFile;
|
||||
|
@ -21,12 +20,16 @@ pub struct Directory {
|
|||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Path> for Directory {
|
||||
type Error = ActixError;
|
||||
type Error = String;
|
||||
fn try_from(directory: &'a Path) -> Result<Self, Self::Error> {
|
||||
let files = glob(directory.join("**").join("*").to_str().ok_or_else(|| {
|
||||
error::ErrorInternalServerError("directory contains invalid characters")
|
||||
})?)
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
let files = glob(
|
||||
directory
|
||||
.join("**")
|
||||
.join("*")
|
||||
.to_str()
|
||||
.ok_or_else(|| String::from("directory contains invalid characters"))?,
|
||||
)
|
||||
.map_err(|e| e.msg)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|path| !path.is_dir())
|
||||
.filter_map(|path| match OsFile::open(&path) {
|
||||
|
@ -58,7 +61,7 @@ mod tests {
|
|||
use std::ffi::OsString;
|
||||
|
||||
#[test]
|
||||
fn test_file_checksum() -> Result<(), ActixError> {
|
||||
fn test_file_checksum() -> Result<(), String> {
|
||||
assert_eq!(
|
||||
Some(OsString::from("rustypaste_logo.png").as_ref()),
|
||||
Directory::try_from(
|
||||
|
|
99
src/main.rs
99
src/main.rs
|
@ -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| {
|
||||
|
|
248
src/paste.rs
248
src/paste.rs
|
@ -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(())
|
||||
}
|
||||
|
|
265
src/server.rs
265
src/server.rs
|
@ -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(), ×tamp).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(), ×tamp).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(())
|
||||
}
|
||||
|
|
42
src/util.rs
42
src/util.rs
|
@ -10,6 +10,7 @@ use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
/// Regex for matching the timestamp extension of a path.
|
||||
pub static TIMESTAMP_EXTENSION_REGEX: Lazy<Regex> = lazy_regex!(r#"\.[0-9]{10,}$"#);
|
||||
|
@ -24,7 +25,7 @@ pub fn get_system_time() -> Result<Duration, ActixError> {
|
|||
/// Returns the first _unexpired_ path matched by a custom glob pattern.
|
||||
///
|
||||
/// The file extension is accepted as a timestamp that points to the expiry date.
|
||||
pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||
pub async fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||
path = PathBuf::from(
|
||||
TIMESTAMP_EXTENSION_REGEX
|
||||
.replacen(
|
||||
|
@ -36,10 +37,15 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
|||
)
|
||||
.to_string(),
|
||||
);
|
||||
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.last()
|
||||
{
|
||||
|
||||
let path_string = path.to_string_lossy().into_owned();
|
||||
let glob_match = match spawn_blocking(move || glob(&format!("{}.[0-9]*", path_string))).await {
|
||||
Ok(Ok(m)) => m.last(),
|
||||
Ok(Err(e)) => return Err(error::ErrorInternalServerError(e)),
|
||||
Err(e) => return Err(error::ErrorInternalServerError(e)),
|
||||
};
|
||||
|
||||
if let Some(glob_path) = glob_match {
|
||||
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
||||
if let Some(extension) = glob_path
|
||||
.extension()
|
||||
|
@ -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(¤t_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(¤t_dir)
|
||||
);
|
||||
fs::remove_file(path)?;
|
||||
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(¤t_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(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue