From da018274198d7fc9aac2c94cf6f6585bb85708f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Sat, 30 Dec 2023 17:59:04 +0100 Subject: [PATCH] code: updating justfile with watch command, adding logging functionality, control+c detection, host/port loading from env, and a file listing example for an alternative source besides databases --- Cargo.lock | 48 ++++++++++ Cargo.toml | 4 + Justfile | 4 + src/admin/views.rs | 2 +- src/bin/list_files_example.rs | 130 ++++++++++++++++++++++++++ src/main.rs | 48 +++++++++- templates/admin/items/list_items.html | 5 + 7 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 src/bin/list_files_example.rs create mode 100644 templates/admin/items/list_items.html diff --git a/Cargo.lock b/Cargo.lock index cd18f13..10a5e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,6 +790,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "either" version = "1.9.0" @@ -808,6 +814,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1243,6 +1262,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -1360,6 +1385,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix 0.38.28", + "windows-sys 0.52.0", +] + [[package]] name = "itertools" version = "0.11.0" @@ -1573,7 +1609,10 @@ dependencies = [ "axum", "barrel", "dotenvy", + "dunce", "entity", + "env_logger", + "log", "mime_guess", "minijinja", "minijinja-autoreload", @@ -3033,6 +3072,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.51" diff --git a/Cargo.toml b/Cargo.toml index 191f70b..a47e1cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "miniweb" version = "0.1.0" edition = "2021" publish = false +default-run = "miniweb" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -42,3 +43,6 @@ rust-embed = { version = "8.0.0", features = [ ] } serde = { version = "1.0.188", features = ["derive"] } tokio = { version = "1.32.0", features = ["full"] } +dunce = "1.0.4" +log = "0.4.20" +env_logger = "0.10.1" diff --git a/Justfile b/Justfile index 116c5c3..ebd95fc 100644 --- a/Justfile +++ b/Justfile @@ -8,6 +8,9 @@ default: run args='miniweb': @cargo run --bin {{args}} +watch: + cargo watch -c -q -w src -x run + status: @echo "Docker Images:" cd docker && docker-compose ls @@ -29,6 +32,7 @@ migrate: # Install Developer dependencies dev-install: cargo install sea-orm-cli + cargo install cargo-watch # Reset Database dev-reset: diff --git a/src/admin/views.rs b/src/admin/views.rs index 261a537..479a787 100644 --- a/src/admin/views.rs +++ b/src/admin/views.rs @@ -152,7 +152,7 @@ pub async fn index_action(Form(example_data): Form) -> impl IntoRes // List Items renders the entire list item page. pub async fn list_item_collection(templates: State) -> impl IntoResponse { - templates.render_html("admin/list_items.html", ()) + templates.render_html("admin/items/list_items.html", ()) } // Items Action is a POST to an item list. By default these are actions, that work on a list of items as input. diff --git a/src/bin/list_files_example.rs b/src/bin/list_files_example.rs new file mode 100644 index 0000000..3cf8fc0 --- /dev/null +++ b/src/bin/list_files_example.rs @@ -0,0 +1,130 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub struct PathEntry { + path: PathBuf, + file_name: String, + is_dir: bool, + is_file: bool, + is_symlink: bool, +} + +impl std::fmt::Display for PathEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path.to_string_lossy()) + } +} + +struct FolderScanner { + base_path: PathBuf, +} + +impl FolderScanner { + // Create a new FolderScanner with a base path + fn new(base_path: &str) -> Self { + FolderScanner { + base_path: PathBuf::from(base_path), + } + } + + // Scan the folder with optional filters + fn scan_folder(&self, folder: &str, filters: Option) -> Vec { + let base_path_clean = dunce::canonicalize(&self.base_path).unwrap(); + let full_path = dunce::canonicalize(self.base_path.join(folder)).unwrap(); + + println!("Scanning folder: {}", &full_path.display()); + + // Ensure that the path is within the base path + if !full_path.starts_with(&base_path_clean) { + panic!("Access to the folder outside the base path is not allowed."); + } + + let entries = fs::read_dir(full_path).unwrap(); + let mut files = Vec::new(); + + for entry in entries { + if let Ok(dir_entry) = entry { + let path = dir_entry.path(); + + if let Some(f) = &filters { + if f.should_filter(&path) { + continue; + } + } + + let (is_dir, is_file, is_simlink) = if let Ok(metadata) = dir_entry.metadata() { + (metadata.is_dir(), metadata.is_file(), metadata.is_symlink()) + } else { + (false, false, false) + }; + + files.push(PathEntry { + path: path, + file_name: dir_entry.file_name().to_string_lossy().into_owned(), + is_dir: is_dir, + is_file: is_file, + is_symlink: is_simlink, + }); + } + } + + files + } +} + +struct Filters { + exclude_dot_files: bool, + exclude_folders: bool, + exclude_symlinks: bool, + allowed_extensions: Vec, +} + +impl Filters { + fn should_filter(&self, path: &Path) -> bool { + if self.exclude_dot_files && path.file_name().unwrap().to_str().unwrap().starts_with('.') { + return true; + } + + if self.exclude_folders && path.is_dir() { + return true; + } + + if self.exclude_symlinks && fs::symlink_metadata(path).unwrap().file_type().is_symlink() { + return true; + } + + if !self.allowed_extensions.is_empty() { + if let Some(ext) = path.extension() { + if !self + .allowed_extensions + .contains(&ext.to_str().unwrap().to_string()) + { + return true; + } + } else { + return true; + } + } + + false + } +} + +fn main() { + let scanner = FolderScanner::new("."); + + // Example usage + let filters = Filters { + exclude_dot_files: false, + exclude_folders: false, + exclude_symlinks: false, + allowed_extensions: vec!["md".to_string(), "rs".to_string()], + }; + + let files = scanner.scan_folder("src", Some(filters)); + for file in files { + println!("{}", file); // Display + println!("{:?}", file); // Debug + } +} diff --git a/src/main.rs b/src/main.rs index 7a08afa..3723ac3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,9 @@ use crate::state::AppState; use axum::{ extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router, }; +use dotenvy::dotenv; +use log::info; +use std::env; use std::net::SocketAddr; use std::sync::Arc; @@ -21,6 +24,11 @@ async fn hello_world(templates: State) -> impl IntoRespons #[tokio::main] async fn main() { + // Load environment + dotenv().ok(); + env_logger::init(); + info!("Miniweb starting..."); + // Prepare App State let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded."); let mut admin = admin::state::AdminRegistry::new("admin"); @@ -43,10 +51,46 @@ async fn main() { .with_state(state); // Run Server - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - println!("listening on {}", addr); + let app_host: std::net::IpAddr = env::var("APP_HOST") + .unwrap_or("127.0.0.1".to_string()) + .parse() + .expect("IP Address expected in APP_HOST"); + let app_port: u16 = env::var("APP_PORT") + .unwrap_or("3000".to_string()) + .parse() + .expect("Port expected in APP_PORT"); + let addr = SocketAddr::from((app_host, app_port)); + info!("listening on {}", addr); + info!("admin on: http://{}/admin", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) + .with_graceful_shutdown(shutdown_signal()) .await .unwrap(); } + +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + info!("shutting down..."); +} diff --git a/templates/admin/items/list_items.html b/templates/admin/items/list_items.html new file mode 100644 index 0000000..82121e2 --- /dev/null +++ b/templates/admin/items/list_items.html @@ -0,0 +1,5 @@ +{% if item_list %} + {% for item in item_list %} + poop: {{ item}} + {% endfor %} +{% endif %} \ No newline at end of file