From c484a59579a7762f9ecb3a5dd39e855b42651308 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 28 Jun 2025 15:03:40 +0300 Subject: [PATCH] implement connections count with template macro, add `--show-hidden` option, fix configuration variable names --- README.md | 7 +++++- src/config.rs | 8 ++++++ src/server/connection.rs | 7 +++++- src/session.rs | 15 ++++++++--- src/session/event.rs | 15 +++++++++++ src/session/event/connection.rs | 44 +++++++++++++++++++++++++++++++++ src/session/storage.rs | 18 +++++++++----- src/session/template.rs | 24 ++++++++++++------ 8 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 src/session/event.rs create mode 100644 src/session/event/connection.rs diff --git a/README.md b/README.md index 65590d8..1d870ed 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ nexy -p /path/to/public_dir -p, --public Absolute path to the public files directory +--show-hidden + Show hidden entries (in the directory listing) + + * Important: this option does not prevent access to hidden files! + --template-access-denied Absolute path to the `Access denied` template file @@ -71,7 +76,7 @@ nexy -p /path/to/public_dir * this template file expects pattern and cannot be in binary format - **Patterns** * `{list}` - entries list for the `public` directory + **Patterns** * `{list}` - entries list for the `public` directory * `{hosts}` - unique visitors count * `{hits}` - requests count --template-index Absolute path to the `Index` template file for each directory diff --git a/src/config.rs b/src/config.rs index ffeb07a..4b75a31 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,12 @@ pub struct Config { #[arg(short, long)] pub public: String, + /// Show hidden entries (in the directory listing) + /// + /// * Important: this option does not prevent access to hidden files! + #[arg(long, default_value_t = false)] + pub show_hidden: bool, + /// Absolute path to the `Access denied` template file /// /// * this template file can be in binary format (e.g. image) @@ -56,6 +62,8 @@ pub struct Config { /// /// **Patterns** /// * `{list}` - entries list for the `public` directory + /// * `{hosts}` - unique visitors count + /// * `{hits}` - requests count #[arg(long)] pub template_welcome: Option, diff --git a/src/server/connection.rs b/src/server/connection.rs index 6e953bf..fe892f5 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -32,6 +32,7 @@ impl Connection { } pub fn handle(mut self) { + self.session.event.connection.update(&self.address.client); let mut t = 0; // total bytes match self.request() { Ok(q) => { @@ -71,7 +72,11 @@ impl Connection { Response::File(b) => b, Response::Directory(ref s, is_root) => { &if is_root { - self.session.template.welcome(Some(s)) + self.session.template.welcome( + Some(s), + Some(self.session.event.connection.hosts()), + Some(self.session.event.connection.hits()), + ) } else { self.session.template.index(Some(s)) } diff --git a/src/session.rs b/src/session.rs index b136c1c..b46f772 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,25 +1,32 @@ mod access_log; mod debug; +mod event; mod storage; mod template; -use {access_log::AccessLog, debug::Debug, storage::Storage, template::Template}; +use {access_log::AccessLog, debug::Debug, event::Event, storage::Storage, template::Template}; /// Shared, multi-thread features for the current server session pub struct Session { - pub debug: Debug, pub access_log: AccessLog, + pub debug: Debug, + pub event: Event, pub storage: Storage, pub template: Template, } impl Session { pub fn init(config: &crate::config::Config) -> anyhow::Result { + let template = Template::init(config)?; Ok(Self { - debug: Debug::init(config)?, access_log: AccessLog::init(config)?, + debug: Debug::init(config)?, + event: Event::init( + // do not init `Connection` event if its features not in use + template.welcome.contains("{hosts}") || template.welcome.contains("{hits}"), + )?, storage: Storage::init(config)?, - template: Template::init(config)?, + template, }) } } diff --git a/src/session/event.rs b/src/session/event.rs new file mode 100644 index 0000000..c2e8386 --- /dev/null +++ b/src/session/event.rs @@ -0,0 +1,15 @@ +mod connection; +use connection::Connection; + +pub struct Event { + pub connection: Connection, + // another features... +} + +impl Event { + pub fn init(is_connection_enabled: bool) -> anyhow::Result { + Ok(Self { + connection: Connection::init(is_connection_enabled), + }) + } +} diff --git a/src/session/event/connection.rs b/src/session/event/connection.rs new file mode 100644 index 0000000..e0b1ec5 --- /dev/null +++ b/src/session/event/connection.rs @@ -0,0 +1,44 @@ +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + sync::RwLock, +}; + +/// Count peer connections (for the current server session) +pub struct Connection(Option>>); + +impl Connection { + pub fn init(is_enabled: bool) -> Self { + if is_enabled { + Self(Some(RwLock::new(HashMap::with_capacity(100)))) + } else { + Self(None) + } + } + + pub fn update(&self, peer: &SocketAddr) { + if let Some(ref this) = self.0 { + this.write() + .unwrap() + .entry(peer.ip()) + .and_modify(|c| *c += 1) + .or_insert(1); + } + } + + pub fn hosts(&self) -> usize { + if let Some(ref this) = self.0 { + this.read().unwrap().len() + } else { + 0 + } + } + + pub fn hits(&self) -> usize { + if let Some(ref this) = self.0 { + this.read().unwrap().values().sum() + } else { + 0 + } + } +} diff --git a/src/session/storage.rs b/src/session/storage.rs index 48d65da..0e43b0e 100644 --- a/src/session/storage.rs +++ b/src/session/storage.rs @@ -18,6 +18,10 @@ pub struct Storage { public_dir: PathBuf, /// Streaming buffer options read_chunk: usize, + /// Show hidden entries (in the directory listing) + /// + /// * important: this option does not prevent access to hidden files! + show_hidden: bool, } impl Storage { @@ -34,6 +38,7 @@ impl Storage { list_config: ListConfig::init(config)?, public_dir, read_chunk: config.read_chunk, + show_hidden: config.show_hidden, }) } @@ -124,18 +129,19 @@ impl Storage { let mut files = Vec::with_capacity(C); for entry in fs::read_dir(path)? { let e = entry?; + let name = e.file_name().to_string_lossy().to_string(); + if !self.show_hidden && name.starts_with('.') { + continue; + } let meta = fs::metadata(e.path())?; match (meta.is_dir(), meta.is_file()) { (true, _) => dirs.push(Dir { meta, - name: e.file_name().to_string_lossy().to_string(), + name, count: fs::read_dir(e.path()).map_or(0, |i| i.count()), }), - (_, true) => files.push(File { - meta, - name: e.file_name().to_string_lossy().to_string(), - }), - _ => {} // @TODO symlinks support? + (_, true) => files.push(File { meta, name }), + _ => continue, // @TODO symlinks support? } } // build resulting list diff --git a/src/session/template.rs b/src/session/template.rs index 80aa860..cbd48ef 100644 --- a/src/session/template.rs +++ b/src/session/template.rs @@ -3,7 +3,7 @@ pub struct Template { index: String, internal_server_error: Vec, not_found: Vec, - welcome: String, + pub welcome: String, } impl Template { @@ -14,21 +14,21 @@ impl Template { Some(ref p) => read(p)?, None => "Access denied".into(), }, - index: match config.template_access_denied { + index: match config.template_index { Some(ref p) => read_to_string(p)?, None => "{list}".into(), }, - internal_server_error: match config.template_access_denied { + internal_server_error: match config.template_internal_server_error { Some(ref p) => read(p)?, None => "Internal server error".into(), }, - not_found: match config.template_access_denied { + not_found: match config.template_not_found { Some(ref p) => read(p)?, None => "Not found".into(), }, - welcome: match config.template_access_denied { + welcome: match config.template_welcome { Some(ref p) => read_to_string(p)?, - None => "Welcome to Nexy!\n\n{list}".into(), + None => "Welcome to Nexy!\n\n{list}\n\nšŸ‘ {hosts} / {hits}".into(), }, }) } @@ -51,9 +51,19 @@ impl Template { &self.not_found } - pub fn welcome(&self, list: Option<&str>) -> Vec { + pub fn welcome( + &self, + list: Option<&str>, + hosts: Option, + hits: Option, + ) -> Vec { + fn format_count(v: Option) -> String { + v.map_or(String::new(), |c| c.to_string()) + } self.welcome .replace("{list}", list.unwrap_or_default()) + .replace("{hosts}", &format_count(hosts)) + .replace("{hits}", &format_count(hits)) .into() } }