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:
|
env:
|
||||||
OUT_DIR: target
|
OUT_DIR: target
|
||||||
- name: Upload reports to codecov
|
- name: Upload reports to codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
name: code-coverage-report
|
name: code-coverage-report
|
||||||
file: lcov.info
|
file: lcov.info
|
||||||
flags: unit-tests
|
flags: unit-tests
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
fixtures:
|
fixtures:
|
||||||
strategy:
|
strategy:
|
||||||
|
|
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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.15.0] - 2024-03-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Allow to override filename when using `random_url` by @tessus in [#233](https://github.com/orhun/rustypaste/pull/233)
|
||||||
|
|
||||||
|
Now you can use the `filename` header to override the name of the uploaded file.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -F "file=@x.txt" -H "filename:override.txt" http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Even if `random_url` is set, the filename will be override.txt
|
||||||
|
|
||||||
|
[`rustypaste-cli`](https://github.com/orhun/rustypaste-cli) also has a new argument for overriding the file name:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rpaste -n filename-on-server.txt awesome.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use more specific HTTP status codes by @tessus in [#262](https://github.com/orhun/rustypaste/pull/262)
|
||||||
|
|
||||||
|
`rustypaste` now returns more appropriate status codes in the following 2 cases (instead of a generic 500 code):
|
||||||
|
|
||||||
|
- If the mime type is on the blacklist: `UnsupportedMediaType` (415)
|
||||||
|
- If the file already exists: `Conflict` (409)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Do path joins more safely by @RealOrangeOne in [#247](https://github.com/orhun/rustypaste/pull/247)
|
||||||
|
- Gracefully exit when there is no config file found by @orhun
|
||||||
|
- Switch to cargo-llvm-cov for code coverage by @orhun in [#260](https://github.com/orhun/rustypaste/pull/260)
|
||||||
|
- Replace unmaintained action by @tessus in [#266](https://github.com/orhun/rustypaste/pull/266)
|
||||||
|
- Set up mergify by @orhun
|
||||||
|
- Apply clippy suggestions by @orhun
|
||||||
|
- Update funding options by @orhun
|
||||||
|
- Update the copyright years by @orhun
|
||||||
|
- Bump dependencies
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Improve logging for deleted file by @tessus in [#235](https://github.com/orhun/rustypaste/pull/235)
|
||||||
|
- Fix deployment by @tessus in [#236](https://github.com/orhun/rustypaste/pull/236)
|
||||||
|
- Return the correct file on multiple files with same name by @tessus in [#234](https://github.com/orhun/rustypaste/pull/234)
|
||||||
|
- Update the hash of the example file by @tessus in [#254](https://github.com/orhun/rustypaste/pull/254)
|
||||||
|
- Error on upload with the same filename by @tessus in [#258](https://github.com/orhun/rustypaste/pull/258)
|
||||||
|
|
||||||
|
### New Contributors
|
||||||
|
|
||||||
|
- @RealOrangeOne made their first contribution in [#247](https://github.com/orhun/rustypaste/pull/247)
|
||||||
|
|
||||||
## [0.14.4] - 2023-12-20
|
## [0.14.4] - 2023-12-20
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rustypaste"
|
name = "rustypaste"
|
||||||
version = "0.14.4"
|
version = "0.15.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A minimal file upload/pastebin service"
|
description = "A minimal file upload/pastebin service"
|
||||||
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
|
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
|
||||||
|
@ -16,17 +16,17 @@ include = ["src/**/*", "Cargo.*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
default = ["rustls"]
|
default = ["rustls"]
|
||||||
openssl = ["actix-web/openssl", "awc/openssl"]
|
openssl = ["actix-web/openssl", "awc/openssl"]
|
||||||
rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
|
rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
|
||||||
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
|
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.4.1" }
|
actix-web = { version = "4.5.1" }
|
||||||
actix-web-grants = { version = "4.0.3" }
|
actix-web-grants = { version = "4.0.3" }
|
||||||
actix-multipart = "0.6.1"
|
actix-multipart = "0.6.1"
|
||||||
actix-files = "0.6.5"
|
actix-files = "0.6.5"
|
||||||
shuttle-actix-web = { version = "0.35.1", optional = true }
|
shuttle-actix-web = { version = "0.42.0", optional = true }
|
||||||
shuttle-runtime = { version = "0.35.1", optional = true }
|
shuttle-runtime = { version = "0.42.0", optional = true }
|
||||||
awc = { version = "3.4.0" }
|
awc = { version = "3.4.0" }
|
||||||
serde = "1.0.196"
|
serde = "1.0.197"
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
petname = { version = "1.1.3", default-features = false, features = [
|
petname = { version = "1.1.3", default-features = false, features = [
|
||||||
"std_rng",
|
"std_rng",
|
||||||
|
@ -36,7 +36,7 @@ rand = "0.8.5"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
regex = "1.10.3"
|
regex = "1.10.4"
|
||||||
serde_regex = "1.1.0"
|
serde_regex = "1.1.0"
|
||||||
lazy-regex = "3.1.0"
|
lazy-regex = "3.1.0"
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
|
@ -44,7 +44,7 @@ humantime-serde = "1.1.1"
|
||||||
glob = "0.3.1"
|
glob = "0.3.1"
|
||||||
ring = "0.17.8"
|
ring = "0.17.8"
|
||||||
hotwatch = "0.5.0"
|
hotwatch = "0.5.0"
|
||||||
tokio = { version = "1.35.1", optional = true }
|
tokio = { version = "1.37.0", features = ["fs"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
uts2ts = "0.4.1"
|
uts2ts = "0.4.1"
|
||||||
|
@ -65,6 +65,7 @@ default-features = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.9.0"
|
actix-rt = "2.9.0"
|
||||||
|
tempfile = "3.10.1"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
|
|
@ -99,6 +99,13 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Packaging status</summary>
|
||||||
|
|
||||||
|
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustypaste.svg)](https://repology.org/project/rustypaste/versions)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### From crates.io
|
### From crates.io
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -5,7 +5,7 @@ use actix_web::http::Method;
|
||||||
use actix_web::middleware::ErrorHandlerResponse;
|
use actix_web::middleware::ErrorHandlerResponse;
|
||||||
use actix_web::{error, web, Error};
|
use actix_web::{error, web, Error};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Extracts the tokens from the authorization header by token type.
|
/// Extracts the tokens from the authorization header by token type.
|
||||||
///
|
///
|
||||||
|
@ -14,8 +14,8 @@ pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<Token
|
||||||
let config = req
|
let config = req
|
||||||
.app_data::<web::Data<RwLock<Config>>>()
|
.app_data::<web::Data<RwLock<Config>>>()
|
||||||
.map(|cfg| cfg.read())
|
.map(|cfg| cfg.read())
|
||||||
.and_then(Result::ok)
|
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?
|
||||||
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?;
|
.await;
|
||||||
|
|
||||||
let mut user_tokens = HashSet::with_capacity(2);
|
let mut user_tokens = HashSet::with_capacity(2);
|
||||||
|
|
||||||
|
|
|
@ -120,13 +120,20 @@ pub struct PasteConfig {
|
||||||
pub delete_expired_files: Option<CleanupConfig>,
|
pub delete_expired_files: Option<CleanupConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default interval for cleanup
|
||||||
|
pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
const fn get_default_cleanup_interval() -> Duration {
|
||||||
|
DEFAULT_CLEANUP_INTERVAL
|
||||||
|
}
|
||||||
|
|
||||||
/// Cleanup configuration.
|
/// Cleanup configuration.
|
||||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct CleanupConfig {
|
pub struct CleanupConfig {
|
||||||
/// Enable cleaning up.
|
/// Enable cleaning up.
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Interval between clean-ups.
|
/// Interval between clean-ups.
|
||||||
#[serde(default, with = "humantime_serde")]
|
#[serde(default = "get_default_cleanup_interval", with = "humantime_serde")]
|
||||||
pub interval: Duration,
|
pub interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
src/file.rs
17
src/file.rs
|
@ -1,5 +1,4 @@
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use actix_web::{error, Error as ActixError};
|
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fs::File as OsFile;
|
use std::fs::File as OsFile;
|
||||||
|
@ -21,12 +20,16 @@ pub struct Directory {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a Path> for Directory {
|
impl<'a> TryFrom<&'a Path> for Directory {
|
||||||
type Error = ActixError;
|
type Error = String;
|
||||||
fn try_from(directory: &'a Path) -> Result<Self, Self::Error> {
|
fn try_from(directory: &'a Path) -> Result<Self, Self::Error> {
|
||||||
let files = glob(directory.join("**").join("*").to_str().ok_or_else(|| {
|
let files = glob(
|
||||||
error::ErrorInternalServerError("directory contains invalid characters")
|
directory
|
||||||
})?)
|
.join("**")
|
||||||
.map_err(error::ErrorInternalServerError)?
|
.join("*")
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| String::from("directory contains invalid characters"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.msg)?
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.filter(|path| !path.is_dir())
|
.filter(|path| !path.is_dir())
|
||||||
.filter_map(|path| match OsFile::open(&path) {
|
.filter_map(|path| match OsFile::open(&path) {
|
||||||
|
@ -58,7 +61,7 @@ mod tests {
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_file_checksum() -> Result<(), ActixError> {
|
fn test_file_checksum() -> Result<(), String> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(OsString::from("rustypaste_logo.png").as_ref()),
|
Some(OsString::from("rustypaste_logo.png").as_ref()),
|
||||||
Directory::try_from(
|
Directory::try_from(
|
||||||
|
|
99
src/main.rs
99
src/main.rs
|
@ -5,7 +5,7 @@ use actix_web::{App, HttpServer};
|
||||||
use awc::ClientBuilder;
|
use awc::ClientBuilder;
|
||||||
use hotwatch::notify::event::ModifyKind;
|
use hotwatch::notify::event::ModifyKind;
|
||||||
use hotwatch::{Event, EventKind, Hotwatch};
|
use hotwatch::{Event, EventKind, Hotwatch};
|
||||||
use rustypaste::config::{Config, ServerConfig};
|
use rustypaste::config::{Config, DEFAULT_CLEANUP_INTERVAL};
|
||||||
use rustypaste::middleware::ContentLengthLimiter;
|
use rustypaste::middleware::ContentLengthLimiter;
|
||||||
use rustypaste::paste::PasteType;
|
use rustypaste::paste::PasteType;
|
||||||
use rustypaste::server;
|
use rustypaste::server;
|
||||||
|
@ -15,9 +15,9 @@ use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Result as IoResult;
|
use std::io::Result as IoResult;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{mpsc, RwLock};
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
#[cfg(not(feature = "shuttle"))]
|
#[cfg(not(feature = "shuttle"))]
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter,
|
filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter,
|
||||||
|
@ -38,7 +38,7 @@ extern crate tracing;
|
||||||
/// * initializes the logger
|
/// * initializes the logger
|
||||||
/// * creates the necessary directories
|
/// * creates the necessary directories
|
||||||
/// * spawns the threads
|
/// * spawns the threads
|
||||||
fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig, Hotwatch)> {
|
async fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, Hotwatch)> {
|
||||||
// Load the .env file.
|
// Load the .env file.
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
||||||
}
|
}
|
||||||
None => config_folder.join("config.toml"),
|
None => config_folder.join("config.toml"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
error!(
|
error!(
|
||||||
"{} is not found, please provide a configuration file.",
|
"{} is not found, please provide a configuration file.",
|
||||||
|
@ -68,17 +69,15 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
||||||
);
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config::parse(&config_path).expect("failed to parse config");
|
let config = Config::parse(&config_path).expect("failed to parse config");
|
||||||
trace!("{:#?}", config);
|
trace!("{:#?}", config);
|
||||||
config.warn_deprecation();
|
config.warn_deprecation();
|
||||||
let server_config = config.server.clone();
|
|
||||||
let paste_config = RwLock::new(config.paste.clone());
|
|
||||||
let (config_sender, config_receiver) = mpsc::channel::<Config>();
|
|
||||||
|
|
||||||
// Create necessary directories.
|
// Create necessary directories.
|
||||||
fs::create_dir_all(&server_config.upload_path)?;
|
fs::create_dir_all(&config.server.upload_path)?;
|
||||||
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||||
fs::create_dir_all(paste_type.get_path(&server_config.upload_path)?)?;
|
fs::create_dir_all(paste_type.get_path(&config.server.upload_path)?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up a watcher for the configuration file changes.
|
// Set up a watcher for the configuration file changes.
|
||||||
|
@ -91,82 +90,71 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
||||||
)
|
)
|
||||||
.expect("failed to initialize configuration file watcher");
|
.expect("failed to initialize configuration file watcher");
|
||||||
|
|
||||||
|
let config_lock = Data::new(RwLock::new(config));
|
||||||
|
|
||||||
// Hot-reload the configuration file.
|
// Hot-reload the configuration file.
|
||||||
let config = Data::new(RwLock::new(config));
|
let config_watcher_config = config_lock.clone();
|
||||||
let cloned_config = Data::clone(&config);
|
|
||||||
let config_watcher = move |event: Event| {
|
let config_watcher = move |event: Event| {
|
||||||
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
|
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
|
||||||
(event.kind, event.paths.first())
|
(event.kind, event.paths.first())
|
||||||
{
|
{
|
||||||
|
info!("Reloading configuration");
|
||||||
|
|
||||||
match Config::parse(path) {
|
match Config::parse(path) {
|
||||||
Ok(config) => match cloned_config.write() {
|
Ok(new_config) => {
|
||||||
Ok(mut cloned_config) => {
|
let mut locked_config = config_watcher_config.blocking_write();
|
||||||
*cloned_config = config.clone();
|
*locked_config = new_config;
|
||||||
info!("Configuration has been updated.");
|
info!("Configuration has been updated.");
|
||||||
if let Err(e) = config_sender.send(config) {
|
locked_config.warn_deprecation();
|
||||||
error!("Failed to send config for the cleanup routine: {}", e)
|
}
|
||||||
}
|
|
||||||
cloned_config.warn_deprecation();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to acquire config: {}", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to update config: {}", e);
|
error!("Failed to update config: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
hotwatch
|
hotwatch
|
||||||
.watch(&config_path, config_watcher)
|
.watch(&config_path, config_watcher)
|
||||||
.unwrap_or_else(|_| panic!("failed to watch {config_path:?}"));
|
.unwrap_or_else(|_| panic!("failed to watch {config_path:?}"));
|
||||||
|
|
||||||
// Create a thread for cleaning up expired files.
|
// Create a thread for cleaning up expired files.
|
||||||
let upload_path = server_config.upload_path.clone();
|
let expired_files_config = config_lock.clone();
|
||||||
|
let mut cleanup_interval = DEFAULT_CLEANUP_INTERVAL;
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || loop {
|
||||||
let mut enabled = false;
|
// Additional context block to ensure the config lock is dropped
|
||||||
if let Some(ref cleanup_config) = paste_config
|
|
||||||
.read()
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.delete_expired_files.clone())
|
|
||||||
{
|
{
|
||||||
if cleanup_config.enabled {
|
let locked_config = expired_files_config.blocking_read();
|
||||||
debug!("Running cleanup...");
|
let upload_path = locked_config.server.upload_path.clone();
|
||||||
for file in util::get_expired_files(&upload_path) {
|
|
||||||
match fs::remove_file(&file) {
|
if let Some(ref cleanup_config) = locked_config.paste.delete_expired_files {
|
||||||
Ok(()) => info!("Removed expired file: {:?}", file),
|
if cleanup_config.enabled {
|
||||||
Err(e) => error!("Cannot remove expired file: {}", e),
|
debug!("Running cleanup...");
|
||||||
|
for file in util::get_expired_files(&upload_path) {
|
||||||
|
match fs::remove_file(&file) {
|
||||||
|
Ok(()) => info!("Removed expired file: {:?}", file),
|
||||||
|
Err(e) => error!("Cannot remove expired file: {}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
cleanup_interval = cleanup_config.interval;
|
||||||
thread::sleep(cleanup_config.interval);
|
|
||||||
}
|
|
||||||
enabled = cleanup_config.enabled;
|
|
||||||
}
|
|
||||||
if let Some(new_config) = if enabled {
|
|
||||||
config_receiver.try_recv().ok()
|
|
||||||
} else {
|
|
||||||
config_receiver.recv().ok()
|
|
||||||
} {
|
|
||||||
match paste_config.write() {
|
|
||||||
Ok(mut paste_config) => {
|
|
||||||
*paste_config = new_config.paste;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to update config for the cleanup routine: {}", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread::sleep(cleanup_interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok((config, server_config, hotwatch))
|
Ok((config_lock, hotwatch))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "shuttle"))]
|
#[cfg(not(feature = "shuttle"))]
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> IoResult<()> {
|
async fn main() -> IoResult<()> {
|
||||||
// Set up the application.
|
// Set up the application.
|
||||||
let (config, server_config, _hotwatch) = setup(&PathBuf::new())?;
|
let (config, _hotwatch) = setup(&PathBuf::new()).await?;
|
||||||
|
|
||||||
|
// Extra context block ensures the lock is stopped
|
||||||
|
let server_config = { config.read().await.server.clone() };
|
||||||
|
|
||||||
// Create an HTTP server.
|
// Create an HTTP server.
|
||||||
let mut http_server = HttpServer::new(move || {
|
let mut http_server = HttpServer::new(move || {
|
||||||
|
@ -203,7 +191,10 @@ async fn main() -> IoResult<()> {
|
||||||
#[shuttle_runtime::main]
|
#[shuttle_runtime::main]
|
||||||
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
|
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
|
||||||
// Set up the application.
|
// Set up the application.
|
||||||
let (config, server_config, _hotwatch) = setup(Path::new("shuttle"))?;
|
let (config, _hotwatch) = setup(Path::new("shuttle"))?;
|
||||||
|
|
||||||
|
// Extra context block ensures the lock is stopped
|
||||||
|
let server_config = { config.read().await.server.clone() };
|
||||||
|
|
||||||
// Create the service.
|
// Create the service.
|
||||||
let service_config = move |cfg: &mut ServiceConfig| {
|
let service_config = move |cfg: &mut ServiceConfig| {
|
||||||
|
|
248
src/paste.rs
248
src/paste.rs
|
@ -5,11 +5,12 @@ use crate::util;
|
||||||
use actix_web::{error, Error};
|
use actix_web::{error, Error};
|
||||||
use awc::Client;
|
use awc::Client;
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::fs::{self, File};
|
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
||||||
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::sync::RwLock;
|
use tokio::fs::{self, File};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Type of the data to store.
|
/// Type of the data to store.
|
||||||
|
@ -92,7 +93,7 @@ impl Paste {
|
||||||
///
|
///
|
||||||
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
||||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||||
pub fn store_file(
|
pub async fn store_file(
|
||||||
&self,
|
&self,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
expiry_date: Option<u128>,
|
expiry_date: Option<u128>,
|
||||||
|
@ -176,6 +177,7 @@ impl Paste {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string();
|
.to_string();
|
||||||
let file_path = util::glob_match_file(path.clone())
|
let file_path = util::glob_match_file(path.clone())
|
||||||
|
.await
|
||||||
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
|
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
|
||||||
if file_path.is_file() && file_path.exists() {
|
if file_path.is_file() && file_path.exists() {
|
||||||
return Err(error::ErrorConflict("file already exists\n"));
|
return Err(error::ErrorConflict("file already exists\n"));
|
||||||
|
@ -183,8 +185,8 @@ impl Paste {
|
||||||
if let Some(timestamp) = expiry_date {
|
if let Some(timestamp) = expiry_date {
|
||||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||||
}
|
}
|
||||||
let mut buffer = File::create(&path)?;
|
let mut buffer = File::create(&path).await?;
|
||||||
buffer.write_all(&self.data)?;
|
buffer.write_all(&self.data).await?;
|
||||||
Ok(file_name)
|
Ok(file_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +202,7 @@ impl Paste {
|
||||||
&mut self,
|
&mut self,
|
||||||
expiry_date: Option<u128>,
|
expiry_date: Option<u128>,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
config: &RwLock<Config>,
|
config: &Config,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?;
|
let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?;
|
||||||
let url = Url::parse(data).map_err(error::ErrorBadRequest)?;
|
let url = Url::parse(data).map_err(error::ErrorBadRequest)?;
|
||||||
|
@ -215,8 +217,6 @@ impl Paste {
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
let payload_limit = config
|
let payload_limit = config
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
|
||||||
.server
|
.server
|
||||||
.max_content_length
|
.max_content_length
|
||||||
.try_into()
|
.try_into()
|
||||||
|
@ -227,15 +227,19 @@ impl Paste {
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?
|
.map_err(error::ErrorInternalServerError)?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let config = config
|
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
|
||||||
let bytes_checksum = util::sha256_digest(&*bytes)?;
|
let bytes_checksum = util::sha256_digest(&*bytes)?;
|
||||||
self.data = bytes;
|
self.data = bytes;
|
||||||
if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() {
|
if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() {
|
||||||
if let Some(file) =
|
let upload_path = config.server.upload_path.clone();
|
||||||
Directory::try_from(config.server.upload_path.as_path())?.get_file(bytes_checksum)
|
|
||||||
{
|
let directory =
|
||||||
|
match spawn_blocking(move || Directory::try_from(upload_path.as_path())).await {
|
||||||
|
Ok(Ok(d)) => d,
|
||||||
|
Ok(Err(e)) => return Err(error::ErrorInternalServerError(e)),
|
||||||
|
Err(e) => return Err(error::ErrorInternalServerError(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(file) = directory.get_file(bytes_checksum) {
|
||||||
return Ok(file
|
return Ok(file
|
||||||
.path
|
.path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -244,7 +248,7 @@ impl Paste {
|
||||||
.to_string());
|
.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.store_file(file_name, expiry_date, None, &config)
|
self.store_file(file_name, expiry_date, None, config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes an URL to a file in upload directory.
|
/// Writes an URL to a file in upload directory.
|
||||||
|
@ -254,7 +258,7 @@ impl Paste {
|
||||||
///
|
///
|
||||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
|
pub async fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
|
||||||
let data = str::from_utf8(&self.data)
|
let data = str::from_utf8(&self.data)
|
||||||
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
||||||
let url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
let url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
||||||
|
@ -269,7 +273,7 @@ impl Paste {
|
||||||
if let Some(timestamp) = expiry_date {
|
if let Some(timestamp) = expiry_date {
|
||||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||||
}
|
}
|
||||||
fs::write(&path, url.to_string())?;
|
fs::write(&path, url.to_string()).await?;
|
||||||
Ok(file_name)
|
Ok(file_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,15 +286,16 @@ mod tests {
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use awc::ClientBuilder;
|
use awc::ClientBuilder;
|
||||||
use byte_unit::Byte;
|
use byte_unit::Byte;
|
||||||
use std::env;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
async fn test_paste_data() -> Result<(), Error> {
|
async fn test_paste() -> Result<(), Error> {
|
||||||
|
let temp_upload_path = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = temp_upload_path.path().to_path_buf();
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
enabled: Some(true),
|
enabled: Some(true),
|
||||||
words: Some(3),
|
words: Some(3),
|
||||||
|
@ -302,16 +307,20 @@ mod tests {
|
||||||
data: vec![65, 66, 67],
|
data: vec![65, 66, 67],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("test.txt", None, None, &config)?;
|
let file_name = paste.store_file("test.txt", None, None, &config).await?;
|
||||||
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
let file_path = temp_upload_path.path().join(file_name);
|
||||||
assert_eq!(
|
assert_eq!("ABC", fs::read_to_string(&file_path).await?);
|
||||||
Some("txt"),
|
assert_eq!(Some("txt"), file_path.extension().and_then(|v| v.to_str()));
|
||||||
PathBuf::from(&file_name)
|
|
||||||
.extension()
|
|
||||||
.and_then(|v| v.to_str())
|
|
||||||
);
|
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
async fn test_paste_random() -> Result<(), Error> {
|
||||||
|
let temp_upload_path = tempdir()?;
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = temp_upload_path.path().to_path_buf();
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
length: Some(4),
|
length: Some(4),
|
||||||
type_: RandomURLType::Alphanumeric,
|
type_: RandomURLType::Alphanumeric,
|
||||||
|
@ -322,11 +331,11 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
let file_name = paste.store_file("foo.tar.gz", None, None, &config).await?;
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
|
||||||
assert!(file_name.ends_with(".tar.gz"));
|
assert!(file_name.ends_with(".tar.gz"));
|
||||||
assert!(file_name.starts_with("foo."));
|
assert!(file_name.starts_with("foo."));
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
length: Some(4),
|
length: Some(4),
|
||||||
|
@ -338,11 +347,11 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
|
let file_name = paste.store_file(".foo.tar.gz", None, None, &config).await?;
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
|
||||||
assert!(file_name.ends_with(".tar.gz"));
|
assert!(file_name.ends_with(".tar.gz"));
|
||||||
assert!(file_name.starts_with(".foo."));
|
assert!(file_name.starts_with(".foo."));
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
length: Some(4),
|
length: Some(4),
|
||||||
|
@ -354,21 +363,30 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
let file_name = paste.store_file("foo.tar.gz", None, None, &config).await?;
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
|
||||||
assert!(file_name.ends_with(".tar.gz"));
|
assert!(file_name.ends_with(".tar.gz"));
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
async fn test_paste_with_extension() -> Result<(), Error> {
|
||||||
|
let temp_upload_path = tempdir()?;
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = temp_upload_path.path().to_path_buf();
|
||||||
config.paste.default_extension = String::from("txt");
|
config.paste.default_extension = String::from("txt");
|
||||||
config.paste.random_url = None;
|
config.paste.random_url = None;
|
||||||
let paste = Paste {
|
let paste = Paste {
|
||||||
data: vec![120, 121, 122],
|
data: vec![120, 121, 122],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file(".foo", None, None, &config)?;
|
let file_name = paste.store_file(".foo", None, None, &config).await?;
|
||||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
|
assert_eq!("xyz", fs::read_to_string(&file_path).await?);
|
||||||
assert_eq!(".foo.txt", file_name);
|
assert_eq!(".foo.txt", file_name);
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
config.paste.default_extension = String::from("bin");
|
config.paste.default_extension = String::from("bin");
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
|
@ -380,16 +398,20 @@ mod tests {
|
||||||
data: vec![120, 121, 122],
|
data: vec![120, 121, 122],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("random", None, None, &config)?;
|
let file_name = paste.store_file("random", None, None, &config).await?;
|
||||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
assert_eq!(
|
assert_eq!(Some("bin"), file_path.extension().and_then(|v| v.to_str()));
|
||||||
Some("bin"),
|
assert_eq!("xyz", fs::read_to_string(&file_path).await?);
|
||||||
PathBuf::from(&file_name)
|
|
||||||
.extension()
|
|
||||||
.and_then(|v| v.to_str())
|
|
||||||
);
|
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
async fn test_paste_filename_from_header() -> Result<(), Error> {
|
||||||
|
let temp_upload_path = tempdir()?;
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = temp_upload_path.path().to_path_buf();
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
length: Some(4),
|
length: Some(4),
|
||||||
type_: RandomURLType::Alphanumeric,
|
type_: RandomURLType::Alphanumeric,
|
||||||
|
@ -400,15 +422,17 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file(
|
let file_name = paste
|
||||||
"filename.txt",
|
.store_file(
|
||||||
None,
|
"filename.txt",
|
||||||
Some("fn_from_header.txt".to_string()),
|
None,
|
||||||
&config,
|
Some("fn_from_header.txt".to_string()),
|
||||||
)?;
|
&config,
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
)
|
||||||
|
.await?;
|
||||||
assert_eq!("fn_from_header.txt", file_name);
|
assert_eq!("fn_from_header.txt", file_name);
|
||||||
fs::remove_file(file_name)?;
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
|
||||||
|
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
length: Some(4),
|
length: Some(4),
|
||||||
|
@ -420,63 +444,107 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file(
|
let file_name = paste
|
||||||
"filename.txt",
|
.store_file(
|
||||||
None,
|
"filename.txt",
|
||||||
Some("fn_from_header".to_string()),
|
None,
|
||||||
&config,
|
Some("fn_from_header".to_string()),
|
||||||
)?;
|
&config,
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
)
|
||||||
|
.await?;
|
||||||
|
let file_path = temp_upload_path.path().join(&file_name);
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_path).await?);
|
||||||
assert_eq!("fn_from_header", file_name);
|
assert_eq!("fn_from_header", file_name);
|
||||||
fs::remove_file(file_name)?;
|
|
||||||
|
|
||||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
Ok(())
|
||||||
fs::create_dir_all(
|
}
|
||||||
paste_type
|
|
||||||
.get_path(&config.server.upload_path)
|
|
||||||
.expect("Bad upload path"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
async fn test_paste_oneshot() -> Result<(), Error> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
config.paste.random_url = None;
|
config.paste.random_url = None;
|
||||||
|
|
||||||
|
fs::create_dir_all(
|
||||||
|
PasteType::Oneshot
|
||||||
|
.get_path(&config.server.upload_path)
|
||||||
|
.expect("Bad upload path"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let paste = Paste {
|
let paste = Paste {
|
||||||
data: vec![116, 101, 115, 116],
|
data: vec![116, 101, 115, 116],
|
||||||
type_: PasteType::Oneshot,
|
type_: PasteType::Oneshot,
|
||||||
};
|
};
|
||||||
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
||||||
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
|
let file_name = paste
|
||||||
|
.store_file("test.file", Some(expiry_date), None, &config)
|
||||||
|
.await?;
|
||||||
let file_path = PasteType::Oneshot
|
let file_path = PasteType::Oneshot
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.expect("Bad upload path")
|
.expect("Bad upload path")
|
||||||
.join(format!("{file_name}.{expiry_date}"));
|
.join(format!("{file_name}.{expiry_date}"));
|
||||||
assert_eq!("test", fs::read_to_string(&file_path)?);
|
assert_eq!("test", fs::read_to_string(&file_path).await?);
|
||||||
fs::remove_file(file_path)?;
|
fs::remove_file(file_path).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
async fn test_paste_url() -> Result<(), Error> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
config.paste.random_url = Some(RandomURLConfig {
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
enabled: Some(true),
|
enabled: Some(true),
|
||||||
..RandomURLConfig::default()
|
..RandomURLConfig::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fs::create_dir_all(
|
||||||
|
PasteType::Url
|
||||||
|
.get_path(&config.server.upload_path)
|
||||||
|
.expect("Bad upload path"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let url = String::from("https://orhun.dev/");
|
let url = String::from("https://orhun.dev/");
|
||||||
let paste = Paste {
|
let paste = Paste {
|
||||||
data: url.as_bytes().to_vec(),
|
data: url.as_bytes().to_vec(),
|
||||||
type_: PasteType::Url,
|
type_: PasteType::Url,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_url(None, &config)?;
|
let file_name = paste.store_url(None, &config).await?;
|
||||||
let file_path = PasteType::Url
|
let file_path = PasteType::Url
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.expect("Bad upload path")
|
.expect("Bad upload path")
|
||||||
.join(&file_name);
|
.join(&file_name);
|
||||||
assert_eq!(url, fs::read_to_string(&file_path)?);
|
assert_eq!(url, fs::read_to_string(&file_path).await?);
|
||||||
fs::remove_file(file_path)?;
|
fs::remove_file(file_path).await?;
|
||||||
|
|
||||||
let url = String::from("testurl.com");
|
let url = String::from("testurl.com");
|
||||||
let paste = Paste {
|
let paste = Paste {
|
||||||
data: url.as_bytes().to_vec(),
|
data: url.as_bytes().to_vec(),
|
||||||
type_: PasteType::Url,
|
type_: PasteType::Url,
|
||||||
};
|
};
|
||||||
assert!(paste.store_url(None, &config).is_err());
|
assert!(paste.store_url(None, &config).await.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
async fn test_paste_remote_url() -> Result<(), Error> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte");
|
config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte");
|
||||||
|
|
||||||
|
fs::create_dir_all(
|
||||||
|
PasteType::Url
|
||||||
|
.get_path(&config.server.upload_path)
|
||||||
|
.expect("Bad upload path"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let url = String::from("https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg");
|
let url = String::from("https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg");
|
||||||
let mut paste = Paste {
|
let mut paste = Paste {
|
||||||
data: url.as_bytes().to_vec(),
|
data: url.as_bytes().to_vec(),
|
||||||
|
@ -487,26 +555,12 @@ mod tests {
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.finish(),
|
.finish(),
|
||||||
);
|
);
|
||||||
let file_name = paste
|
let _ = paste.store_remote_file(None, &client_data, &config).await?;
|
||||||
.store_remote_file(None, &client_data, &RwLock::new(config.clone()))
|
|
||||||
.await?;
|
|
||||||
let file_path = PasteType::RemoteFile
|
|
||||||
.get_path(&config.server.upload_path)
|
|
||||||
.expect("Bad upload path")
|
|
||||||
.join(file_name);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
||||||
util::sha256_digest(&*paste.data)?
|
util::sha256_digest(&*paste.data)?
|
||||||
);
|
);
|
||||||
fs::remove_file(file_path)?;
|
|
||||||
|
|
||||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
|
||||||
fs::remove_dir(
|
|
||||||
paste_type
|
|
||||||
.get_path(&config.server.upload_path)
|
|
||||||
.expect("Bad upload path"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
265
src/server.rs
265
src/server.rs
|
@ -18,20 +18,17 @@ use mime::TEXT_PLAIN_UTF_8;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use uts2ts;
|
use uts2ts;
|
||||||
|
|
||||||
/// Shows the landing page.
|
/// Shows the landing page.
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||||
let mut config = config
|
let mut config = config.read().await.clone();
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
|
||||||
.clone();
|
|
||||||
let redirect = HttpResponse::Found()
|
let redirect = HttpResponse::Found()
|
||||||
.append_header(("Location", env!("CARGO_PKG_HOMEPAGE")))
|
.append_header(("Location", env!("CARGO_PKG_HOMEPAGE")))
|
||||||
.finish();
|
.finish();
|
||||||
|
@ -53,7 +50,7 @@ async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error>
|
||||||
}
|
}
|
||||||
if let Some(mut landing_page) = config.landing_page {
|
if let Some(mut landing_page) = config.landing_page {
|
||||||
if let Some(file) = landing_page.file {
|
if let Some(file) = landing_page.file {
|
||||||
landing_page.text = fs::read_to_string(file).ok();
|
landing_page.text = fs::read_to_string(file).await.ok();
|
||||||
}
|
}
|
||||||
match landing_page.text {
|
match landing_page.text {
|
||||||
Some(page) => Ok(HttpResponse::Ok()
|
Some(page) => Ok(HttpResponse::Ok()
|
||||||
|
@ -86,15 +83,14 @@ async fn serve(
|
||||||
options: Option<web::Query<ServeOptions>>,
|
options: Option<web::Query<ServeOptions>>,
|
||||||
config: web::Data<RwLock<Config>>,
|
config: web::Data<RwLock<Config>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let config = config
|
let config = config.read().await;
|
||||||
.read()
|
let mut path =
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?).await?;
|
||||||
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
|
||||||
let mut paste_type = PasteType::File;
|
let mut paste_type = PasteType::File;
|
||||||
if !path.exists() || path.is_dir() {
|
if !path.exists() || path.is_dir() {
|
||||||
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||||
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
|
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
|
||||||
let alt_path = util::glob_match_file(alt_path)?;
|
let alt_path = util::glob_match_file(alt_path).await?;
|
||||||
if alt_path.exists()
|
if alt_path.exists()
|
||||||
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
|
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
|
||||||
{
|
{
|
||||||
|
@ -128,21 +124,23 @@ async fn serve(
|
||||||
file,
|
file,
|
||||||
util::get_system_time()?.as_millis()
|
util::get_system_time()?.as_millis()
|
||||||
)),
|
)),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
PasteType::Url => Ok(HttpResponse::Found()
|
PasteType::Url => Ok(HttpResponse::Found()
|
||||||
.append_header(("Location", fs::read_to_string(&path)?))
|
.append_header(("Location", fs::read_to_string(&path).await?))
|
||||||
.finish()),
|
.finish()),
|
||||||
PasteType::OneshotUrl => {
|
PasteType::OneshotUrl => {
|
||||||
let resp = HttpResponse::Found()
|
let resp = HttpResponse::Found()
|
||||||
.append_header(("Location", fs::read_to_string(&path)?))
|
.append_header(("Location", fs::read_to_string(&path).await?))
|
||||||
.finish();
|
.finish();
|
||||||
fs::rename(
|
fs::rename(
|
||||||
&path,
|
&path,
|
||||||
path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())),
|
path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,14 +153,12 @@ async fn delete(
|
||||||
file: web::Path<String>,
|
file: web::Path<String>,
|
||||||
config: web::Data<RwLock<Config>>,
|
config: web::Data<RwLock<Config>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let config = config
|
let config = config.read().await;
|
||||||
.read()
|
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?).await?;
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
|
||||||
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
|
||||||
if !path.is_file() || !path.exists() {
|
if !path.is_file() || !path.exists() {
|
||||||
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
||||||
}
|
}
|
||||||
match fs::remove_file(path) {
|
match fs::remove_file(path).await {
|
||||||
Ok(_) => info!("deleted file: {:?}", file.to_string()),
|
Ok(_) => info!("deleted file: {:?}", file.to_string()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("cannot delete file: {}", e);
|
error!("cannot delete file: {}", e);
|
||||||
|
@ -176,9 +172,7 @@ async fn delete(
|
||||||
#[get("/version")]
|
#[get("/version")]
|
||||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||||
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||||
let config = config
|
let config = config.read().await;
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
|
||||||
if !config.server.expose_version.unwrap_or(false) {
|
if !config.server.expose_version.unwrap_or(false) {
|
||||||
warn!("server is not configured to expose version endpoint");
|
warn!("server is not configured to expose version endpoint");
|
||||||
Err(error::ErrorNotFound(""))?;
|
Err(error::ErrorNotFound(""))?;
|
||||||
|
@ -199,13 +193,8 @@ async fn upload(
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let connection = request.connection_info().clone();
|
let connection = request.connection_info().clone();
|
||||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||||
let server_url = match config
|
let config = config.read().await;
|
||||||
.read()
|
let server_url = match config.server.url.clone() {
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
|
||||||
.server
|
|
||||||
.url
|
|
||||||
.clone()
|
|
||||||
{
|
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
format!("{}://{}", connection.scheme(), connection.host(),)
|
format!("{}://{}", connection.scheme(), connection.host(),)
|
||||||
|
@ -215,8 +204,6 @@ async fn upload(
|
||||||
let mut expiry_date = header::parse_expiry_date(request.headers(), time)?;
|
let mut expiry_date = header::parse_expiry_date(request.headers(), time)?;
|
||||||
if expiry_date.is_none() {
|
if expiry_date.is_none() {
|
||||||
expiry_date = config
|
expiry_date = config
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
|
||||||
.paste
|
.paste
|
||||||
.default_expiry
|
.default_expiry
|
||||||
.and_then(|v| time.checked_add(v).map(|t| t.as_millis()));
|
.and_then(|v| time.checked_add(v).map(|t| t.as_millis()));
|
||||||
|
@ -239,18 +226,11 @@ async fn upload(
|
||||||
&& paste_type != PasteType::RemoteFile
|
&& paste_type != PasteType::RemoteFile
|
||||||
&& paste_type != PasteType::OneshotUrl
|
&& paste_type != PasteType::OneshotUrl
|
||||||
&& expiry_date.is_none()
|
&& expiry_date.is_none()
|
||||||
&& !config
|
&& !config.paste.duplicate_files.unwrap_or(true)
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
|
||||||
.paste
|
|
||||||
.duplicate_files
|
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
{
|
||||||
let bytes_checksum = util::sha256_digest(&*bytes)?;
|
let bytes_checksum = util::sha256_digest(&*bytes)?;
|
||||||
let config = config
|
if let Some(file) = Directory::try_from(config.server.upload_path.as_path())
|
||||||
.read()
|
.map_err(error::ErrorInternalServerError)?
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
|
||||||
if let Some(file) = Directory::try_from(config.server.upload_path.as_path())?
|
|
||||||
.get_file(bytes_checksum)
|
.get_file(bytes_checksum)
|
||||||
{
|
{
|
||||||
urls.push(format!(
|
urls.push(format!(
|
||||||
|
@ -270,15 +250,14 @@ async fn upload(
|
||||||
};
|
};
|
||||||
let mut file_name = match paste.type_ {
|
let mut file_name = match paste.type_ {
|
||||||
PasteType::File | PasteType::Oneshot => {
|
PasteType::File | PasteType::Oneshot => {
|
||||||
let config = config
|
paste
|
||||||
.read()
|
.store_file(
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
content.get_file_name()?,
|
||||||
paste.store_file(
|
expiry_date,
|
||||||
content.get_file_name()?,
|
header_filename,
|
||||||
expiry_date,
|
&config,
|
||||||
header_filename,
|
)
|
||||||
&config,
|
.await?
|
||||||
)?
|
|
||||||
}
|
}
|
||||||
PasteType::RemoteFile => {
|
PasteType::RemoteFile => {
|
||||||
paste
|
paste
|
||||||
|
@ -286,10 +265,7 @@ async fn upload(
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
PasteType::Url | PasteType::OneshotUrl => {
|
PasteType::Url | PasteType::OneshotUrl => {
|
||||||
let config = config
|
paste.store_url(expiry_date, &config).await?
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
|
||||||
paste.store_url(expiry_date, &config)?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
info!(
|
info!(
|
||||||
|
@ -300,9 +276,6 @@ async fn upload(
|
||||||
.get_appropriate_unit(UnitType::Decimal),
|
.get_appropriate_unit(UnitType::Decimal),
|
||||||
host
|
host
|
||||||
);
|
);
|
||||||
let config = config
|
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
|
||||||
if let Some(handle_spaces_config) = config.server.handle_spaces {
|
if let Some(handle_spaces_config) = config.server.handle_spaces {
|
||||||
file_name = handle_spaces_config.process_filename(&file_name);
|
file_name = handle_spaces_config.process_filename(&file_name);
|
||||||
}
|
}
|
||||||
|
@ -330,53 +303,58 @@ pub struct ListItem {
|
||||||
#[get("/list")]
|
#[get("/list")]
|
||||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||||
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||||
let config = config
|
let config = config.read().await;
|
||||||
.read()
|
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
|
||||||
.clone();
|
|
||||||
if !config.server.expose_list.unwrap_or(false) {
|
if !config.server.expose_list.unwrap_or(false) {
|
||||||
warn!("server is not configured to expose list endpoint");
|
warn!("server is not configured to expose list endpoint");
|
||||||
Err(error::ErrorNotFound(""))?;
|
return Err(error::ErrorNotFound(""));
|
||||||
}
|
}
|
||||||
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
|
|
||||||
.filter_map(|entry| {
|
let mut entries: Vec<ListItem> = Vec::new();
|
||||||
entry.ok().and_then(|e| {
|
|
||||||
let metadata = match e.metadata() {
|
let mut dir_contents = fs::read_dir(&config.server.upload_path).await?;
|
||||||
Ok(metadata) => {
|
|
||||||
if metadata.is_dir() {
|
let system_time = util::get_system_time()?;
|
||||||
return None;
|
|
||||||
}
|
while let Ok(Some(entry)) = dir_contents.next_entry().await {
|
||||||
metadata
|
let metadata = match entry.metadata().await {
|
||||||
}
|
Ok(metadata) if metadata.is_dir() => continue,
|
||||||
Err(e) => {
|
Ok(metadata) => metadata,
|
||||||
error!("failed to read metadata: {e}");
|
Err(e) => {
|
||||||
return None;
|
error!("failed to read metadata: {e}");
|
||||||
}
|
continue;
|
||||||
};
|
}
|
||||||
let mut file_name = PathBuf::from(e.file_name());
|
};
|
||||||
let expires_at_utc = if let Some(expiration) = file_name
|
|
||||||
.extension()
|
let mut file_name = PathBuf::from(entry.file_name());
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.and_then(|v| v.parse::<i64>().ok())
|
let expires_at_utc = match file_name
|
||||||
{
|
.extension()
|
||||||
file_name.set_extension("");
|
.and_then(|ext| ext.to_str())
|
||||||
if util::get_system_time().ok()?
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
> Duration::from_millis(expiration.try_into().ok()?)
|
{
|
||||||
{
|
Some(expiration) if system_time > Duration::from_millis(expiration) => continue,
|
||||||
return None;
|
Some(expiration) => {
|
||||||
}
|
file_name.set_extension("");
|
||||||
Some(uts2ts::uts2ts(expiration / 1000).as_string())
|
Some(
|
||||||
} else {
|
uts2ts::uts2ts(
|
||||||
None
|
(expiration / 1000)
|
||||||
};
|
.try_into()
|
||||||
Some(ListItem {
|
.map_err(|_| error::ErrorInternalServerError("Invalid timestamp"))?,
|
||||||
file_name,
|
)
|
||||||
file_size: metadata.len(),
|
.as_string(),
|
||||||
expires_at_utc,
|
)
|
||||||
})
|
}
|
||||||
})
|
None => None,
|
||||||
})
|
};
|
||||||
.collect();
|
|
||||||
|
entries.push(ListItem {
|
||||||
|
file_name,
|
||||||
|
file_size: metadata.len(),
|
||||||
|
expires_at_utc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(entries))
|
Ok(HttpResponse::Ok().json(entries))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +398,7 @@ mod tests {
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest {
|
fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest {
|
||||||
let multipart_data = format!(
|
let multipart_data = format!(
|
||||||
|
@ -523,7 +502,7 @@ mod tests {
|
||||||
let response = test::call_service(&app, request).await;
|
let response = test::call_service(&app, request).await;
|
||||||
assert_eq!(StatusCode::OK, response.status());
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
assert_body(response.into_body(), "landing page from file").await?;
|
assert_body(response.into_body(), "landing page from file").await?;
|
||||||
fs::remove_file(filename)?;
|
fs::remove_file(filename).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,7 +605,7 @@ mod tests {
|
||||||
config.server.expose_list = Some(true);
|
config.server.expose_list = Some(true);
|
||||||
|
|
||||||
let test_upload_dir = "test_upload";
|
let test_upload_dir = "test_upload";
|
||||||
fs::create_dir(test_upload_dir)?;
|
fs::create_dir(test_upload_dir).await?;
|
||||||
config.server.upload_path = PathBuf::from(test_upload_dir);
|
config.server.upload_path = PathBuf::from(test_upload_dir);
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
|
@ -657,7 +636,7 @@ mod tests {
|
||||||
PathBuf::from(filename)
|
PathBuf::from(filename)
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::remove_dir_all(test_upload_dir)?;
|
fs::remove_dir_all(test_upload_dir).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -668,7 +647,7 @@ mod tests {
|
||||||
config.server.expose_list = Some(true);
|
config.server.expose_list = Some(true);
|
||||||
|
|
||||||
let test_upload_dir = "test_upload";
|
let test_upload_dir = "test_upload";
|
||||||
fs::create_dir(test_upload_dir)?;
|
fs::create_dir(test_upload_dir).await?;
|
||||||
config.server.upload_path = PathBuf::from(test_upload_dir);
|
config.server.upload_path = PathBuf::from(test_upload_dir);
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
|
@ -702,7 +681,7 @@ mod tests {
|
||||||
|
|
||||||
assert!(result.is_empty());
|
assert!(result.is_empty());
|
||||||
|
|
||||||
fs::remove_dir_all(test_upload_dir)?;
|
fs::remove_dir_all(test_upload_dir).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -752,9 +731,10 @@ mod tests {
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_delete_file() -> Result<(), Error> {
|
async fn test_delete_file() -> Result<(), Error> {
|
||||||
|
let temp_upload_path = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.delete_tokens = Some(["test".to_string()].into());
|
config.server.delete_tokens = Some(["test".to_string()].into());
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = temp_upload_path.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -781,8 +761,7 @@ mod tests {
|
||||||
assert_eq!(StatusCode::OK, response.status());
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
assert_body(response.into_body(), "file deleted\n").await?;
|
assert_body(response.into_body(), "file deleted\n").await?;
|
||||||
|
|
||||||
let path = PathBuf::from(file_name);
|
assert!(!temp_upload_path.path().join(file_name).exists());
|
||||||
assert!(!path.exists());
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -790,7 +769,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_delete_file_without_token_in_config() -> Result<(), Error> {
|
async fn test_delete_file_without_token_in_config() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -815,8 +794,9 @@ mod tests {
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_file() -> Result<(), Error> {
|
async fn test_upload_file() -> Result<(), Error> {
|
||||||
|
let test_delete_file = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = test_delete_file.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -847,7 +827,7 @@ mod tests {
|
||||||
assert_eq!(StatusCode::OK, response.status());
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
assert_body(response.into_body(), ×tamp).await?;
|
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()
|
let serve_request = TestRequest::get()
|
||||||
.uri(&format!("/{file_name}"))
|
.uri(&format!("/{file_name}"))
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -859,8 +839,9 @@ mod tests {
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_file_override_filename() -> Result<(), Error> {
|
async fn test_upload_file_override_filename() -> Result<(), Error> {
|
||||||
|
let test_delete_file = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = test_delete_file.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -897,7 +878,7 @@ mod tests {
|
||||||
assert_eq!(StatusCode::OK, response.status());
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
assert_body(response.into_body(), ×tamp).await?;
|
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()
|
let serve_request = TestRequest::get()
|
||||||
.uri(&format!("/{header_filename}"))
|
.uri(&format!("/{header_filename}"))
|
||||||
.to_request();
|
.to_request();
|
||||||
|
@ -909,8 +890,9 @@ mod tests {
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_same_filename() -> Result<(), Error> {
|
async fn test_upload_same_filename() -> Result<(), Error> {
|
||||||
|
let temp_upload_dir = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = temp_upload_dir.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -954,7 +936,7 @@ mod tests {
|
||||||
assert_eq!(StatusCode::CONFLICT, response.status());
|
assert_eq!(StatusCode::CONFLICT, response.status());
|
||||||
assert_body(response.into_body(), "file already exists\n").await?;
|
assert_body(response.into_body(), "file already exists\n").await?;
|
||||||
|
|
||||||
fs::remove_file(header_filename)?;
|
fs::remove_file(temp_upload_dir.path().join(header_filename)).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -963,7 +945,7 @@ mod tests {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
||||||
let test_upload_dir = "test_upload";
|
let test_upload_dir = "test_upload";
|
||||||
fs::create_dir(test_upload_dir)?;
|
fs::create_dir(test_upload_dir).await?;
|
||||||
|
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = PathBuf::from(&test_upload_dir);
|
config.server.upload_path = PathBuf::from(&test_upload_dir);
|
||||||
|
@ -1002,15 +984,16 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(first_body_bytes, second_body_bytes);
|
assert_eq!(first_body_bytes, second_body_bytes);
|
||||||
|
|
||||||
fs::remove_dir_all(test_upload_dir)?;
|
fs::remove_dir_all(test_upload_dir).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_expiring_file() -> Result<(), Error> {
|
async fn test_upload_expiring_file() -> Result<(), Error> {
|
||||||
|
let temp_upload_path = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = temp_upload_path.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -1054,11 +1037,16 @@ mod tests {
|
||||||
let response = test::call_service(&app, serve_request).await;
|
let response = test::call_service(&app, serve_request).await;
|
||||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
|
|
||||||
if let Some(glob_path) = glob(&format!("{file_name}.[0-9]*"))
|
if let Some(glob_path) = glob(
|
||||||
.map_err(error::ErrorInternalServerError)?
|
&temp_upload_path
|
||||||
.next()
|
.path()
|
||||||
|
.join(format!("{file_name}.[0-9]*"))
|
||||||
|
.to_string_lossy(),
|
||||||
|
)
|
||||||
|
.map_err(error::ErrorInternalServerError)?
|
||||||
|
.next()
|
||||||
{
|
{
|
||||||
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?;
|
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1066,8 +1054,9 @@ mod tests {
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_remote_file() -> Result<(), Error> {
|
async fn test_upload_remote_file() -> Result<(), Error> {
|
||||||
|
let temp_upload_dir = tempdir()?;
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = temp_upload_dir.path().to_path_buf();
|
||||||
config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default();
|
config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
|
@ -1113,7 +1102,7 @@ mod tests {
|
||||||
util::sha256_digest(&*body_bytes)?
|
util::sha256_digest(&*body_bytes)?
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::remove_file(file_name)?;
|
fs::remove_file(temp_upload_dir.path().join(file_name)).await?;
|
||||||
|
|
||||||
let serve_request = TestRequest::get()
|
let serve_request = TestRequest::get()
|
||||||
.uri(&format!("/{file_name}"))
|
.uri(&format!("/{file_name}"))
|
||||||
|
@ -1127,7 +1116,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_url() -> Result<(), Error> {
|
async fn test_upload_url() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -1140,7 +1129,7 @@ mod tests {
|
||||||
let url_upload_path = PasteType::Url
|
let url_upload_path = PasteType::Url
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.expect("Bad upload path");
|
.expect("Bad upload path");
|
||||||
fs::create_dir_all(&url_upload_path)?;
|
fs::create_dir_all(&url_upload_path).await?;
|
||||||
|
|
||||||
let response = test::call_service(
|
let response = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
|
@ -1154,8 +1143,8 @@ mod tests {
|
||||||
let response = test::call_service(&app, serve_request).await;
|
let response = test::call_service(&app, serve_request).await;
|
||||||
assert_eq!(StatusCode::FOUND, response.status());
|
assert_eq!(StatusCode::FOUND, response.status());
|
||||||
|
|
||||||
fs::remove_file(url_upload_path.join("url"))?;
|
fs::remove_file(url_upload_path.join("url")).await?;
|
||||||
fs::remove_dir(url_upload_path)?;
|
fs::remove_dir(url_upload_path).await?;
|
||||||
|
|
||||||
let serve_request = TestRequest::get().uri("/url").to_request();
|
let serve_request = TestRequest::get().uri("/url").to_request();
|
||||||
let response = test::call_service(&app, serve_request).await;
|
let response = test::call_service(&app, serve_request).await;
|
||||||
|
@ -1167,7 +1156,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_oneshot() -> Result<(), Error> {
|
async fn test_upload_oneshot() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -1180,7 +1169,7 @@ mod tests {
|
||||||
let oneshot_upload_path = PasteType::Oneshot
|
let oneshot_upload_path = PasteType::Oneshot
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.expect("Bad upload path");
|
.expect("Bad upload path");
|
||||||
fs::create_dir_all(&oneshot_upload_path)?;
|
fs::create_dir_all(&oneshot_upload_path).await?;
|
||||||
|
|
||||||
let file_name = "oneshot.txt";
|
let file_name = "oneshot.txt";
|
||||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||||
|
@ -1217,9 +1206,9 @@ mod tests {
|
||||||
.map_err(error::ErrorInternalServerError)?
|
.map_err(error::ErrorInternalServerError)?
|
||||||
.next()
|
.next()
|
||||||
{
|
{
|
||||||
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?;
|
fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?).await?;
|
||||||
}
|
}
|
||||||
fs::remove_dir(oneshot_upload_path)?;
|
fs::remove_dir(oneshot_upload_path).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1227,7 +1216,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_upload_oneshot_url() -> Result<(), Error> {
|
async fn test_upload_oneshot_url() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = tempdir()?.path().to_path_buf();
|
||||||
|
|
||||||
let oneshot_url_suffix = "oneshot_url";
|
let oneshot_url_suffix = "oneshot_url";
|
||||||
|
|
||||||
|
@ -1242,7 +1231,7 @@ mod tests {
|
||||||
let url_upload_path = PasteType::OneshotUrl
|
let url_upload_path = PasteType::OneshotUrl
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.expect("Bad upload path");
|
.expect("Bad upload path");
|
||||||
fs::create_dir_all(&url_upload_path)?;
|
fs::create_dir_all(&url_upload_path).await?;
|
||||||
|
|
||||||
let response = test::call_service(
|
let response = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
|
@ -1272,7 +1261,7 @@ mod tests {
|
||||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs::remove_dir_all(url_upload_path)?;
|
fs::remove_dir_all(url_upload_path).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
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::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
|
||||||
/// Regex for matching the timestamp extension of a path.
|
/// Regex for matching the timestamp extension of a path.
|
||||||
pub static TIMESTAMP_EXTENSION_REGEX: Lazy<Regex> = lazy_regex!(r#"\.[0-9]{10,}$"#);
|
pub static TIMESTAMP_EXTENSION_REGEX: Lazy<Regex> = lazy_regex!(r#"\.[0-9]{10,}$"#);
|
||||||
|
@ -24,7 +25,7 @@ pub fn get_system_time() -> Result<Duration, ActixError> {
|
||||||
/// Returns the first _unexpired_ path matched by a custom glob pattern.
|
/// Returns the first _unexpired_ path matched by a custom glob pattern.
|
||||||
///
|
///
|
||||||
/// The file extension is accepted as a timestamp that points to the expiry date.
|
/// The file extension is accepted as a timestamp that points to the expiry date.
|
||||||
pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
pub async fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||||
path = PathBuf::from(
|
path = PathBuf::from(
|
||||||
TIMESTAMP_EXTENSION_REGEX
|
TIMESTAMP_EXTENSION_REGEX
|
||||||
.replacen(
|
.replacen(
|
||||||
|
@ -36,10 +37,15 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||||
)
|
)
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
|
|
||||||
.map_err(error::ErrorInternalServerError)?
|
let path_string = path.to_string_lossy().into_owned();
|
||||||
.last()
|
let glob_match = match spawn_blocking(move || glob(&format!("{}.[0-9]*", path_string))).await {
|
||||||
{
|
Ok(Ok(m)) => m.last(),
|
||||||
|
Ok(Err(e)) => return Err(error::ErrorInternalServerError(e)),
|
||||||
|
Err(e) => return Err(error::ErrorInternalServerError(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(glob_path) = glob_match {
|
||||||
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
||||||
if let Some(extension) = glob_path
|
if let Some(extension) = glob_path
|
||||||
.extension()
|
.extension()
|
||||||
|
@ -137,6 +143,8 @@ mod tests {
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_time() -> Result<(), ActixError> {
|
fn test_system_time() -> Result<(), ActixError> {
|
||||||
let system_time = get_system_time()?.as_millis();
|
let system_time = get_system_time()?.as_millis();
|
||||||
|
@ -145,19 +153,19 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[actix_rt::test]
|
||||||
fn test_glob_match() -> Result<(), ActixError> {
|
async fn test_glob_match() -> Result<(), ActixError> {
|
||||||
let path = PathBuf::from(format!(
|
let path = PathBuf::from(format!(
|
||||||
"expired.file1.{}",
|
"expired.file1.{}",
|
||||||
get_system_time()?.as_millis() + 50
|
get_system_time()?.as_millis() + 50
|
||||||
));
|
));
|
||||||
fs::write(&path, String::new())?;
|
fs::write(&path, String::new())?;
|
||||||
assert_eq!(path, glob_match_file(PathBuf::from("expired.file1"))?);
|
assert_eq!(path, glob_match_file(PathBuf::from("expired.file1")).await?);
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(75));
|
thread::sleep(Duration::from_millis(75));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PathBuf::from("expired.file1"),
|
PathBuf::from("expired.file1"),
|
||||||
glob_match_file(PathBuf::from("expired.file1"))?
|
glob_match_file(PathBuf::from("expired.file1")).await?
|
||||||
);
|
);
|
||||||
fs::remove_file(path)?;
|
fs::remove_file(path)?;
|
||||||
|
|
||||||
|
@ -179,18 +187,16 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_expired_files() -> Result<(), ActixError> {
|
fn test_get_expired_files() -> Result<(), ActixError> {
|
||||||
let current_dir = env::current_dir()?;
|
let test_temp_dir = tempdir()?;
|
||||||
|
let test_dir = test_temp_dir.path();
|
||||||
let expiration_time = get_system_time()?.as_millis() + 50;
|
let expiration_time = get_system_time()?.as_millis() + 50;
|
||||||
let path = PathBuf::from(format!("expired.file2.{expiration_time}"));
|
let path = test_dir.join(format!("expired.file2.{expiration_time}"));
|
||||||
fs::write(&path, String::new())?;
|
fs::write(&path, String::new())?;
|
||||||
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(¤t_dir));
|
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(test_dir));
|
||||||
thread::sleep(Duration::from_millis(75));
|
thread::sleep(Duration::from_millis(75));
|
||||||
assert_eq!(
|
assert_eq!(vec![path.clone()], get_expired_files(test_dir));
|
||||||
vec![current_dir.join(&path)],
|
fs::remove_file(&path)?;
|
||||||
get_expired_files(¤t_dir)
|
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(test_dir));
|
||||||
);
|
|
||||||
fs::remove_file(path)?;
|
|
||||||
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(¤t_dir));
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue