mirror of
https://github.com/amigan/rustypaste-pretty.git
synced 2025-01-31 13:02:43 -05:00
feat(server): support auto-deletion of expired files (#17)
feat(server): support auto-deletion of expired files (#17) chore(ci): set the number of test threads to 1 feat(config): allow the real-time update of cleanup routine docs(readme): update README.md about deleting expired files
This commit is contained in:
parent
a3e266b8b4
commit
dd91c50d50
6 changed files with 115 additions and 11 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -47,7 +47,7 @@ jobs:
|
||||||
tar -xzf cargo-tarpaulin-*.tar.gz
|
tar -xzf cargo-tarpaulin-*.tar.gz
|
||||||
mv cargo-tarpaulin ~/.cargo/bin/
|
mv cargo-tarpaulin ~/.cargo/bin/
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo tarpaulin --out Xml --verbose
|
run: cargo tarpaulin --out Xml --verbose -- --test-threads 1
|
||||||
- name: Upload reports to codecov
|
- name: Upload reports to codecov
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -20,6 +20,7 @@ some text
|
||||||
- pet name (e.g. `capital-mosquito.txt`)
|
- pet name (e.g. `capital-mosquito.txt`)
|
||||||
- alphanumeric string (e.g. `yB84D2Dv.txt`)
|
- alphanumeric string (e.g. `yB84D2Dv.txt`)
|
||||||
- supports expiring links
|
- supports expiring links
|
||||||
|
- auto-deletion of expired files (optional)
|
||||||
- supports one shot links (can only be viewed once)
|
- supports one shot links (can only be viewed once)
|
||||||
- guesses MIME types
|
- guesses MIME types
|
||||||
- supports overriding and blacklisting
|
- supports overriding and blacklisting
|
||||||
|
@ -113,6 +114,10 @@ $ curl -F "remote=https://example.com/file.png" "<server_address>"
|
||||||
|
|
||||||
#### Cleaning up expired files
|
#### Cleaning up expired files
|
||||||
|
|
||||||
|
Configure `delete_expired_files` to set an interval for deleting the expired files automatically.
|
||||||
|
|
||||||
|
On the other hand, following script can be used as [cron](https://en.wikipedia.org/wiki/Cron) for cleaning up the expired files manually:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
#!/bin/env sh
|
#!/bin/env sh
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
|
|
|
@ -27,3 +27,4 @@ mime_blacklist = [
|
||||||
"application/java-vm"
|
"application/java-vm"
|
||||||
]
|
]
|
||||||
duplicate_files = false
|
duplicate_files = false
|
||||||
|
delete_expired_files = { enabled = true, interval = "1h" }
|
||||||
|
|
|
@ -56,6 +56,18 @@ pub struct PasteConfig {
|
||||||
pub mime_blacklist: Vec<String>,
|
pub mime_blacklist: Vec<String>,
|
||||||
/// Allow duplicate uploads
|
/// Allow duplicate uploads
|
||||||
pub duplicate_files: Option<bool>,
|
pub duplicate_files: Option<bool>,
|
||||||
|
/// Delete expired files.
|
||||||
|
pub delete_expired_files: Option<CleanupConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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")]
|
||||||
|
pub interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -6,12 +6,14 @@ use hotwatch::{Event, Hotwatch};
|
||||||
use rustypaste::config::Config;
|
use rustypaste::config::Config;
|
||||||
use rustypaste::paste::PasteType;
|
use rustypaste::paste::PasteType;
|
||||||
use rustypaste::server;
|
use rustypaste::server;
|
||||||
|
use rustypaste::util;
|
||||||
use rustypaste::CONFIG_ENV;
|
use rustypaste::CONFIG_ENV;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Result as IoResult;
|
use std::io::Result as IoResult;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::RwLock;
|
use std::sync::{mpsc, RwLock};
|
||||||
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
|
@ -30,6 +32,8 @@ async fn main() -> IoResult<()> {
|
||||||
};
|
};
|
||||||
let config = Config::parse(&config_path).expect("failed to parse config");
|
let config = Config::parse(&config_path).expect("failed to parse config");
|
||||||
let server_config = config.server.clone();
|
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(&server_config.upload_path)?;
|
||||||
|
@ -55,15 +59,18 @@ async fn main() -> IoResult<()> {
|
||||||
match Config::parse(&path) {
|
match Config::parse(&path) {
|
||||||
Ok(config) => match cloned_config.write() {
|
Ok(config) => match cloned_config.write() {
|
||||||
Ok(mut cloned_config) => {
|
Ok(mut cloned_config) => {
|
||||||
*cloned_config = config;
|
*cloned_config = config.clone();
|
||||||
log::info!("Configuration has been updated.");
|
log::info!("Configuration has been updated.");
|
||||||
|
if let Err(e) = config_sender.send(config) {
|
||||||
|
log::error!("Failed to send config for the cleanup routine: {}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to acquire configuration: {}", e);
|
log::error!("Failed to acquire config: {}", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to update configuration: {}", e);
|
log::error!("Failed to update config: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +79,43 @@ async fn main() -> IoResult<()> {
|
||||||
.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 HTTP server.
|
// Create a thread for cleaning up expired files.
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
if cleanup_config.enabled {
|
||||||
|
log::debug!("Running cleanup...");
|
||||||
|
for file in util::get_expired_files(&server_config.upload_path) {
|
||||||
|
match fs::remove_file(&file) {
|
||||||
|
Ok(()) => log::info!("Removed expired file: {:?}", file),
|
||||||
|
Err(e) => log::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) => {
|
||||||
|
log::error!("Failed to update config for the cleanup routine: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an HTTP server.
|
||||||
let mut http_server = HttpServer::new(move || {
|
let mut http_server = HttpServer::new(move || {
|
||||||
let http_client = ClientBuilder::new()
|
let http_client = ClientBuilder::new()
|
||||||
.timeout(
|
.timeout(
|
||||||
|
|
53
src/util.rs
53
src/util.rs
|
@ -1,9 +1,10 @@
|
||||||
|
use crate::paste::PasteType;
|
||||||
use actix_web::{error, Error as ActixError};
|
use actix_web::{error, Error as ActixError};
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
use lazy_regex::{lazy_regex, Lazy, Regex};
|
use lazy_regex::{lazy_regex, Lazy, Regex};
|
||||||
use ring::digest::{Context, SHA256};
|
use ring::digest::{Context, SHA256};
|
||||||
use std::io::{BufReader, Read};
|
use std::io::{BufReader, Read};
|
||||||
use std::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};
|
||||||
|
|
||||||
|
@ -50,6 +51,30 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the found expired files in the possible upload locations.
|
||||||
|
///
|
||||||
|
/// Fail-safe, omits errors.
|
||||||
|
pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
|
||||||
|
[PasteType::File, PasteType::Oneshot, PasteType::Url]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok())
|
||||||
|
.flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>())
|
||||||
|
.filter(|path| {
|
||||||
|
if let Some(extension) = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|v| v.to_str())
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
{
|
||||||
|
get_system_time()
|
||||||
|
.map(|system_time| system_time > Duration::from_millis(extension))
|
||||||
|
.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the SHA256 digest of the given input.
|
/// Returns the SHA256 digest of the given input.
|
||||||
pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
|
pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
|
||||||
let mut reader = BufReader::new(input);
|
let mut reader = BufReader::new(input);
|
||||||
|
@ -76,6 +101,7 @@ pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -89,16 +115,16 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_glob_match() -> Result<(), ActixError> {
|
fn test_glob_match() -> Result<(), ActixError> {
|
||||||
let path = PathBuf::from(format!(
|
let path = PathBuf::from(format!(
|
||||||
"expired.file.{}",
|
"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.file"))?);
|
assert_eq!(path, glob_match_file(PathBuf::from("expired.file1"))?);
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(75));
|
thread::sleep(Duration::from_millis(75));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PathBuf::from("expired.file"),
|
PathBuf::from("expired.file1"),
|
||||||
glob_match_file(PathBuf::from("expired.file"))?
|
glob_match_file(PathBuf::from("expired.file1"))?
|
||||||
);
|
);
|
||||||
fs::remove_file(path)?;
|
fs::remove_file(path)?;
|
||||||
|
|
||||||
|
@ -117,4 +143,21 @@ mod tests {
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_expired_files() -> Result<(), ActixError> {
|
||||||
|
let current_dir = env::current_dir()?;
|
||||||
|
let expiration_time = get_system_time()?.as_millis() + 50;
|
||||||
|
let path = PathBuf::from(format!("expired.file2.{}", expiration_time));
|
||||||
|
fs::write(&path, String::new())?;
|
||||||
|
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(¤t_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));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue