feat(config): hot-reload the configuration

This commit is contained in:
Orhun Parmaksız 2021-11-07 16:06:57 +03:00
parent f078a9afa7
commit a2de1c3334
No known key found for this signature in database
GPG key ID: F83424824B3E4B90
6 changed files with 176 additions and 18 deletions

118
Cargo.lock generated
View file

@ -652,6 +652,18 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "filetime"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.20" version = "1.0.20"
@ -680,6 +692,25 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags",
"fsevent-sys",
]
[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "fuchsia-zircon" name = "fuchsia-zircon"
version = "0.3.3" version = "0.3.3"
@ -880,6 +911,16 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "hotwatch"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39301670a6f5798b75f36a1b149a379a50df5aa7c71be50f4b41ec6eab445cb8"
dependencies = [
"log",
"notify",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.4" version = "0.2.4"
@ -930,6 +971,26 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea70330449622910e0edebab230734569516269fb32342fb0a8956340fa48c6c" checksum = "ea70330449622910e0edebab230734569516269fb32342fb0a8956340fa48c6c"
[[package]]
name = "inotify"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.10" version = "0.1.10"
@ -1029,6 +1090,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "lexical-core" name = "lexical-core"
version = "0.7.6" version = "0.7.6"
@ -1144,6 +1211,18 @@ dependencies = [
"winapi 0.2.8", "winapi 0.2.8",
] ]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log",
"mio",
"slab",
]
[[package]] [[package]]
name = "mio-uds" name = "mio-uds"
version = "0.6.8" version = "0.6.8"
@ -1199,6 +1278,24 @@ dependencies = [
"version_check 0.9.3", "version_check 0.9.3",
] ]
[[package]]
name = "notify"
version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
dependencies = [
"bitflags",
"filetime",
"fsevent",
"fsevent-sys",
"inotify",
"libc",
"mio",
"mio-extras",
"walkdir",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.0"
@ -1549,6 +1646,7 @@ dependencies = [
"env_logger", "env_logger",
"futures-util", "futures-util",
"glob", "glob",
"hotwatch",
"humantime", "humantime",
"infer", "infer",
"lazy-regex", "lazy-regex",
@ -1569,6 +1667,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -2166,6 +2273,17 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.9.0+wasi-snapshot-preview1" version = "0.9.0+wasi-snapshot-preview1"

View file

@ -31,6 +31,7 @@ lazy-regex = "2.2.1"
humantime = "2.1.0" humantime = "2.1.0"
glob = "0.3.0" glob = "0.3.0"
ring = "0.16.20" ring = "0.16.20"
hotwatch = "0.4.5"
[dependencies.config] [dependencies.config]
version = "0.11.0" version = "0.11.0"

View file

@ -14,7 +14,7 @@ some text
## Features ## Features
- File upload & URL shortening - File upload & URL shortening & upload from URL
- supports basic HTTP authentication - supports basic HTTP authentication
- random file names (optional) - random file names (optional)
- pet name (e.g. `capital-mosquito.txt`) - pet name (e.g. `capital-mosquito.txt`)
@ -26,6 +26,8 @@ some text
- no duplicate uploads (optional) - no duplicate uploads (optional)
- Single binary - Single binary
- [binary releases](https://github.com/orhun/rustypaste/releases) - [binary releases](https://github.com/orhun/rustypaste/releases)
- Simple configuration
- supports hot reloading
- Easy to deploy - Easy to deploy
- [docker images](https://hub.docker.com/r/orhunp/rustypaste) - [docker images](https://hub.docker.com/r/orhunp/rustypaste)
- No database - No database
@ -184,7 +186,7 @@ http {
### Roadmap ### Roadmap
- Hot reload the configuration file _Nothing here yet! 🎉_
### Contributing ### Contributing

View file

@ -2,7 +2,7 @@ use crate::mime::MimeMatcher;
use crate::random::RandomURLConfig; use crate::random::RandomURLConfig;
use byte_unit::Byte; use byte_unit::Byte;
use config::{self, ConfigError}; use config::{self, ConfigError};
use std::path::PathBuf; use std::path::{Path, PathBuf};
/// Configuration values. /// Configuration values.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
@ -43,10 +43,10 @@ pub struct PasteConfig {
impl Config { impl Config {
/// Parses the config file and returns the values. /// Parses the config file and returns the values.
pub fn parse(file_name: &str) -> Result<Config, ConfigError> { pub fn parse(path: &Path) -> Result<Config, ConfigError> {
let mut config = config::Config::default(); let mut config = config::Config::default();
config config
.merge(config::File::with_name(file_name))? .merge(config::File::from(path))?
.merge(config::Environment::new().separator("__"))?; .merge(config::Environment::new().separator("__"))?;
config.try_into() config.try_into()
} }
@ -59,13 +59,9 @@ mod tests {
#[test] #[test]
fn test_parse_config() -> Result<(), ConfigError> { fn test_parse_config() -> Result<(), ConfigError> {
let file_name = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml");
.join("config.toml")
.to_str()
.unwrap()
.to_string();
env::set_var("SERVER__ADDRESS", "0.0.1.1"); env::set_var("SERVER__ADDRESS", "0.0.1.1");
let config = Config::parse(&file_name)?; let config = Config::parse(&config_path)?;
assert_eq!("0.0.1.1", config.server.address); assert_eq!("0.0.1.1", config.server.address);
Ok(()) Ok(())
} }

View file

@ -1,39 +1,78 @@
use actix_web::client::ClientBuilder; use actix_web::client::ClientBuilder;
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{App, HttpServer}; use actix_web::{App, HttpServer};
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 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::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
#[actix_web::main] #[actix_web::main]
async fn main() -> IoResult<()> { async fn main() -> IoResult<()> {
dotenv::dotenv().ok(); // Initialize logger.
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let config = Config::parse(env::var("CONFIG").as_deref().unwrap_or("config"))
.expect("failed to parse config"); // Parse configuration.
let server_config = config.server.clone(); dotenv::dotenv().ok();
let config_path =
PathBuf::from(env::var("CONFIG").unwrap_or_else(|_| String::from("config.toml")));
let config = Arc::new(Mutex::new(
Config::parse(&config_path).expect("failed to parse config"),
));
let cloned_config = Arc::clone(&config);
let server_config = config.lock().expect("cannot acquire config").server.clone();
// Create necessary directories.
fs::create_dir_all(&server_config.upload_path)?; fs::create_dir_all(&server_config.upload_path)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] { for paste_type in &[PasteType::Url, PasteType::Oneshot] {
fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?; fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?;
} }
// Set up a watcher for the configuration file changes.
let mut hotwatch = Hotwatch::new_with_custom_delay(Duration::from_secs(1))
.expect("failed to initialize configuration file watcher");
// Hot-reload the configuration file.
hotwatch
.watch(&config_path, move |event: Event| {
if let Event::Write(path) = event {
match Config::parse(&path) {
Ok(config) => {
*cloned_config.lock().expect("cannot acquire config") = config;
log::info!("Configuration has been updated.");
}
Err(e) => {
log::error!("Failed to update configuration: {}", e);
}
}
}
})
.unwrap_or_else(|_| panic!("failed to watch {:?}", config_path));
// Create a HTTP server.
let mut http_server = HttpServer::new(move || { let mut http_server = HttpServer::new(move || {
let http_client = ClientBuilder::default() let http_client = ClientBuilder::default()
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.disable_redirects() .disable_redirects()
.finish(); .finish();
App::new() App::new()
.data(config.clone()) .data(Arc::clone(&config))
.data(http_client) .data(http_client)
.wrap(Logger::default()) .wrap(Logger::default())
.configure(server::configure_routes) .configure(server::configure_routes)
}) })
.bind(server_config.address)?; .bind(server_config.address)?;
// Set worker count for the server.
if let Some(workers) = server_config.workers { if let Some(workers) = server_config.workers {
http_server = http_server.workers(workers); http_server = http_server.workers(workers);
} }
// Run the server.
http_server.run().await http_server.run().await
} }

View file

@ -14,6 +14,7 @@ use futures_util::stream::StreamExt;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::env; use std::env;
use std::fs; use std::fs;
use std::sync::{Arc, Mutex};
/// Shows the landing page. /// Shows the landing page.
#[get("/")] #[get("/")]
@ -81,13 +82,14 @@ async fn upload(
request: HttpRequest, request: HttpRequest,
mut payload: Multipart, mut payload: Multipart,
client: web::Data<Client>, client: web::Data<Client>,
config: web::Data<Config>, config: web::Data<Arc<Mutex<Config>>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let connection = request.connection_info(); let connection = request.connection_info();
let host = connection.remote_addr().unwrap_or("unknown host"); let host = connection.remote_addr().unwrap_or("unknown host");
auth::check(host, request.headers(), env::var("AUTH_TOKEN").ok())?; auth::check(host, request.headers(), env::var("AUTH_TOKEN").ok())?;
let expiry_date = header::parse_expiry_date(request.headers())?; let expiry_date = header::parse_expiry_date(request.headers())?;
let mut urls: Vec<String> = Vec::new(); let mut urls: Vec<String> = Vec::new();
let config = config.lock().expect("cannot acquire config");
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field = item?; let mut field = item?;
let content = ContentDisposition::try_from(field.content_disposition())?; let content = ContentDisposition::try_from(field.content_disposition())?;
@ -96,7 +98,7 @@ async fn upload(
while let Some(chunk) = field.next().await { while let Some(chunk) = field.next().await {
bytes.append(&mut chunk?.to_vec()); bytes.append(&mut chunk?.to_vec());
if bytes.len() as u128 > config.server.max_content_length.get_bytes() { if bytes.len() as u128 > config.server.max_content_length.get_bytes() {
log::warn!("upload rejected for {}", host); log::warn!("Upload rejected for {}", host);
return Err(error::ErrorPayloadTooLarge("upload limit exceeded")); return Err(error::ErrorPayloadTooLarge("upload limit exceeded"));
} }
} }