From 64db7d12e5901d14fa29b89c085073bb4953c1a4 Mon Sep 17 00:00:00 2001 From: cosmod Date: Tue, 10 Sep 2024 15:59:49 +0800 Subject: [PATCH] Add the parse_path method in the Application trait --- zino-core/src/application/mod.rs | 103 +++++++++++++----- .../src/application/tracing_subscriber.rs | 2 +- zino-core/src/auth/rego_engine.rs | 2 +- zino-core/src/connector/arrow/mod.rs | 8 +- zino-core/src/i18n/mod.rs | 2 +- zino-core/src/openapi/mod.rs | 2 +- zino-core/src/orm/manager.rs | 3 +- zino-core/src/state/mod.rs | 8 +- zino-core/src/view/minijinja.rs | 4 +- zino-core/src/view/mod.rs | 12 +- zino-core/src/view/tera.rs | 9 +- zino/src/application/actix_cluster.rs | 17 +-- zino/src/application/axum_cluster.rs | 19 +--- zino/src/application/dioxus_desktop.rs | 9 +- zino/src/application/ntex_cluster.rs | 14 +-- 15 files changed, 116 insertions(+), 98 deletions(-) diff --git a/zino-core/src/application/mod.rs b/zino-core/src/application/mod.rs index 6f04d271..c96fa198 100644 --- a/zino-core/src/application/mod.rs +++ b/zino-core/src/application/mod.rs @@ -45,9 +45,15 @@ use crate::{ trace::TraceContext, LazyLock, Map, }; +use ahash::{HashMap, HashMapExt}; use reqwest::Response; use serde::de::DeserializeOwned; -use std::{env, fs, path::PathBuf, thread}; +use std::{ + borrow::Cow, + env, fs, + path::{Path, PathBuf}, + thread, +}; use toml::value::Table; #[cfg(feature = "openapi")] @@ -117,14 +123,11 @@ pub trait Application { crate::view::init::(); // Initializes the directories to ensure that they are ready for use - if let Some(dirs) = SHARED_APP_STATE.get_config("dirs") { - for dir in dirs.values().filter_map(|v| v.as_str()) { - let path = parse_path(dir); - if !path.exists() { - if let Err(err) = fs::create_dir_all(&path) { - let path = path.display(); - tracing::error!("fail to create the directory {path}: {err}"); - } + for path in SHARED_DIRS.values() { + if !path.exists() { + if let Err(err) = fs::create_dir_all(&path) { + let path = path.display(); + tracing::error!("fail to create the directory {path}: {err}"); } } } @@ -233,35 +236,57 @@ pub trait Application { APP_DOMAIN.as_ref() } + /// Returns the secret key for the application. + /// It should have at least 64 bytes. + /// + /// # Note + /// + /// This should only be used for internal services. Do not expose it to external users. + #[inline] + fn secret_key() -> &'static [u8] { + SECRET_KEY.get().expect("fail to get the secret key") + } + /// Returns the project directory for the application. #[inline] fn project_dir() -> &'static PathBuf { LazyLock::force(&PROJECT_DIR) } - /// Returns the secret key for the application. - /// It should have at least 64 bytes. + /// Returns the config directory for the application. /// /// # Note /// - /// This should only be used for internal services. Do not expose it to external users. + /// The default config directory is `${PROJECT_DIR}/config`. + /// It can also be specified by the environment variable `ZINO_APP_CONFIG_DIR`. #[inline] - fn secret_key() -> &'static [u8] { - SECRET_KEY.get().expect("fail to get the secret key") + fn config_dir() -> &'static PathBuf { + LazyLock::force(&CONFIG_DIR) } - /// Returns the shared directory with the specific name, + /// Returns the shared directory with a specific name, /// which is defined in the `dirs` table. - fn shared_dir(name: &str) -> PathBuf { - let path = if let Some(path) = SHARED_APP_STATE - .get_config("dirs") - .and_then(|t| t.get_str(name)) - { - path - } else { - name - }; - Self::project_dir().join(path) + /// + /// # Examples + /// + /// ```toml + /// [dirs] + /// data = "/data/zino" # an absolute path + /// cache = "~/zino/cache" # a path in the home dir + /// assets = "local/assets" # a path in the project dir + /// ``` + #[inline] + fn shared_dir(name: &str) -> Cow<'_, PathBuf> { + SHARED_DIRS + .get(name) + .map(|path| Cow::Borrowed(path)) + .unwrap_or_else(|| Cow::Owned(Self::parse_path(name))) + } + + /// Parses an absolute path, or a path relative to the home dir `~/` or project dir. + #[inline] + fn parse_path(path: &str) -> PathBuf { + join_path(&PROJECT_DIR, path) } /// Spawns a new thread to run cron jobs. @@ -331,18 +356,18 @@ pub trait Application { } } -/// Parses a path relative to the project dir. -pub(crate) fn parse_path(path: &str) -> PathBuf { +/// Joins a path to the specific dir. +pub(crate) fn join_path(dir: &Path, path: &str) -> PathBuf { if path.starts_with('/') { path.into() } else if let Some(path) = path.strip_prefix("~/") { if let Some(home_dir) = dirs::home_dir() { home_dir.join(path) } else { - PROJECT_DIR.join(path) + dir.join(path) } } else { - PROJECT_DIR.join(path) + dir.join(path) } } @@ -392,6 +417,26 @@ pub(crate) static PROJECT_DIR: LazyLock = LazyLock::new(|| { }) }); +/// The config directory. +pub(crate) static CONFIG_DIR: LazyLock = LazyLock::new(|| { + env::var("ZINO_APP_CONFIG_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PROJECT_DIR.join("config")) +}); + +/// Shared directories. +static SHARED_DIRS: LazyLock> = LazyLock::new(|| { + let mut dirs = HashMap::new(); + if let Some(config) = SHARED_APP_STATE.get_config("dirs") { + for (key, value) in config { + if let Some(path) = value.as_str() { + dirs.insert(key.to_owned(), join_path(&PROJECT_DIR, path)); + } + } + } + dirs +}); + /// Shared app state. static SHARED_APP_STATE: LazyLock> = LazyLock::new(|| { let mut state = State::default(); diff --git a/zino-core/src/application/tracing_subscriber.rs b/zino-core/src/application/tracing_subscriber.rs index aa1d8ec7..1a991add 100644 --- a/zino-core/src/application/tracing_subscriber.rs +++ b/zino-core/src/application/tracing_subscriber.rs @@ -100,7 +100,7 @@ pub(super) fn init() { flatten_event = config.get_bool("flatten-event").unwrap_or(false); } - let log_dir = super::parse_path(log_dir); + let log_dir = APP::parse_path(log_dir); if !log_dir.exists() { fs::create_dir(log_dir.as_path()).unwrap_or_else(|err| { let log_dir = log_dir.display(); diff --git a/zino-core/src/auth/rego_engine.rs b/zino-core/src/auth/rego_engine.rs index 6f2c4d39..c35ab7b8 100644 --- a/zino-core/src/auth/rego_engine.rs +++ b/zino-core/src/auth/rego_engine.rs @@ -110,7 +110,7 @@ impl RegoEngine { /// Shared Rego evaluation engine. static SHARED_REGO_ENGINE: LazyLock = LazyLock::new(|| { let engine = RegoEngine::new(); - let opa_dir = application::PROJECT_DIR.join("./config/opa"); + let opa_dir = application::CONFIG_DIR.join("opa"); match fs::read_dir(opa_dir) { Ok(entries) => { let files = entries.filter_map(|entry| entry.ok()); diff --git a/zino-core/src/connector/arrow/mod.rs b/zino-core/src/connector/arrow/mod.rs index 2c27b243..78e4ca46 100644 --- a/zino-core/src/connector/arrow/mod.rs +++ b/zino-core/src/connector/arrow/mod.rs @@ -2,7 +2,7 @@ use super::{Connector, DataSource, DataSourceConnector::Arrow}; use crate::{ - application::{http_client, PROJECT_DIR}, + application::{self, http_client, PROJECT_DIR}, bail, error::Error, extension::TomlTableExt, @@ -64,7 +64,7 @@ impl ArrowConnector { pub fn new() -> Self { Self { context: OnceLock::new(), - root: PROJECT_DIR.join("local/data/"), + root: PROJECT_DIR.to_owned(), tables: None, system_variables: ScalarValueProvider::default(), user_defined_variables: ScalarValueProvider::default(), @@ -73,14 +73,14 @@ impl ArrowConnector { /// Creates a new instance with the configuration. pub fn with_config(config: &Table) -> Self { - let root = config.get_str("root").unwrap_or("local/data/"); + let root = config.get_str("root").unwrap_or_default(); let mut system_variables = ScalarValueProvider::default(); if let Some(variables) = config.get_table("variables") { system_variables.read_toml_table(variables); } Self { context: OnceLock::new(), - root: PROJECT_DIR.join(root), + root: application::join_path(&PROJECT_DIR, root), tables: config.get_array("tables").cloned(), system_variables, user_defined_variables: ScalarValueProvider::default(), diff --git a/zino-core/src/i18n/mod.rs b/zino-core/src/i18n/mod.rs index 67537534..cc8d2475 100644 --- a/zino-core/src/i18n/mod.rs +++ b/zino-core/src/i18n/mod.rs @@ -66,7 +66,7 @@ type Translation = FluentBundle; /// Localization. static LOCALIZATION: LazyLock> = LazyLock::new(|| { let mut locales = Vec::new(); - let locale_dir = application::PROJECT_DIR.join("./config/locale"); + let locale_dir = application::CONFIG_DIR.join("locale"); match fs::read_dir(locale_dir) { Ok(entries) => { let files = entries.filter_map(|entry| entry.ok()); diff --git a/zino-core/src/openapi/mod.rs b/zino-core/src/openapi/mod.rs index 4e485872..2ddd9886 100644 --- a/zino-core/src/openapi/mod.rs +++ b/zino-core/src/openapi/mod.rs @@ -226,7 +226,7 @@ pub(crate) fn default_external_docs() -> Option { /// OpenAPI paths. static OPENAPI_PATHS: LazyLock> = LazyLock::new(|| { let mut paths: BTreeMap = BTreeMap::new(); - let openapi_dir = application::PROJECT_DIR.join("./config/openapi"); + let openapi_dir = application::CONFIG_DIR.join("openapi"); match fs::read_dir(openapi_dir) { Ok(entries) => { let mut openapi_tags = Vec::new(); diff --git a/zino-core/src/orm/manager.rs b/zino-core/src/orm/manager.rs index ffe4d8bd..c0f934b8 100644 --- a/zino-core/src/orm/manager.rs +++ b/zino-core/src/orm/manager.rs @@ -171,6 +171,7 @@ cfg_if::cfg_if! { connect_options } } else { + use crate::application::{self, PROJECT_DIR}; use sqlx::sqlite::SqliteConnectOptions; /// Options and flags which can be used to configure a SQLite connection. @@ -180,7 +181,7 @@ cfg_if::cfg_if! { connect_options = connect_options.read_only(read_only); } - let database_path = crate::application::parse_path(database); + let database_path = application::join_path(&PROJECT_DIR, database); connect_options.filename(database_path) } } diff --git a/zino-core/src/state/mod.rs b/zino-core/src/state/mod.rs index 11295f21..552741b2 100644 --- a/zino-core/src/state/mod.rs +++ b/zino-core/src/state/mod.rs @@ -100,6 +100,8 @@ impl State { /// Loads the config according to the specific env. /// + /// # Note + /// /// It supports the `json` or `toml` format of configuration source data, /// which can be specified by the environment variable `ZINO_APP_CONFIG_FORMAT`. /// By default, it reads the config from a local file. If `ZINO_APP_CONFIG_URL` is set, @@ -115,10 +117,10 @@ impl State { let format = std::env::var("ZINO_APP_CONFIG_FORMAT") .map(|s| s.to_ascii_lowercase()) .unwrap_or_else(|_| "toml".to_owned()); - let config_file_dir = application::PROJECT_DIR.join("config"); - if config_file_dir.exists() { + let config_dir = &application::CONFIG_DIR; + if config_dir.exists() { let config_file = format!("config.{env}.{format}"); - let config_file_path = config_file_dir.join(&config_file); + let config_file_path = config_dir.join(&config_file); config::read_config_file(&config_file_path, env).unwrap_or_else(|err| { tracing::error!("fail to read the config file `{config_file}`: {err}"); Table::new() diff --git a/zino-core/src/view/minijinja.rs b/zino-core/src/view/minijinja.rs index 0f00c73b..6e737cc7 100644 --- a/zino-core/src/view/minijinja.rs +++ b/zino-core/src/view/minijinja.rs @@ -1,7 +1,7 @@ use crate::{error::Error, state::State, warn, Map}; use convert_case::{Case, Casing}; use minijinja::Environment; -use std::sync::OnceLock; +use std::{path::PathBuf, sync::OnceLock}; /// Renders a template with the given data using [`minijinja`](https://crates.io/crates/minijinja). pub fn render(template_name: &str, data: Map) -> Result { @@ -13,7 +13,7 @@ pub fn render(template_name: &str, data: Map) -> Result { } /// Loads templates. -pub(crate) fn load_templates(app_state: &'static State, template_dir: String) { +pub(crate) fn load_templates(app_state: &'static State, template_dir: PathBuf) { let mut view_engine = Environment::new(); let app_env = app_state.env(); view_engine.set_debug(app_env.is_dev()); diff --git a/zino-core/src/view/mod.rs b/zino-core/src/view/mod.rs index 72e29bf6..0964dfd0 100644 --- a/zino-core/src/view/mod.rs +++ b/zino-core/src/view/mod.rs @@ -10,7 +10,6 @@ //! | `view-tera` | Enables the `tera` template engine. | No | use crate::{application::Application, extension::TomlTableExt}; -use std::path::Path; cfg_if::cfg_if! { if #[cfg(feature = "view-tera")] { @@ -35,14 +34,5 @@ pub(crate) fn init() { template_dir = dir; } } - - let template_dir = if Path::new(template_dir).exists() { - template_dir.to_owned() - } else { - APP::project_dir() - .join("templates") - .to_string_lossy() - .into() - }; - load_templates(app_state, template_dir); + load_templates(app_state, APP::parse_path(template_dir)); } diff --git a/zino-core/src/view/tera.rs b/zino-core/src/view/tera.rs index 8da93148..9d0442c2 100644 --- a/zino-core/src/view/tera.rs +++ b/zino-core/src/view/tera.rs @@ -1,5 +1,5 @@ use crate::{error::Error, state::State, warn, Map}; -use std::sync::OnceLock; +use std::{path::PathBuf, sync::OnceLock}; use tera::{Context, Tera}; /// Renders a template with the given data using [`tera`](https://crates.io/crates/tera). @@ -14,10 +14,9 @@ pub fn render(template_name: &str, data: Map) -> Result { } /// Loads templates. -pub(crate) fn load_templates(app_state: &'static State, template_dir: String) { - let template_dir_glob = template_dir + "/**/*"; - let mut view_engine = - Tera::new(template_dir_glob.as_str()).expect("fail to parse html templates"); +pub(crate) fn load_templates(app_state: &'static State, template_dir: PathBuf) { + let dir_glob = template_dir.to_string_lossy().into_owned() + "/**/*"; + let mut view_engine = Tera::new(dir_glob.as_str()).expect("fail to parse html templates"); view_engine.autoescape_on(vec![".html", ".html.tera", ".tera"]); if app_state.env().is_dev() { view_engine diff --git a/zino/src/application/actix_cluster.rs b/zino/src/application/actix_cluster.rs index 816c527e..fdb66b12 100644 --- a/zino/src/application/actix_cluster.rs +++ b/zino/src/application/actix_cluster.rs @@ -8,7 +8,7 @@ use actix_web::{ web::{self, FormConfig, JsonConfig, PayloadConfig}, App, HttpServer, Responder, }; -use std::{fs, path::PathBuf, time::Duration}; +use std::{fs, time::Duration}; use utoipa_rapidoc::RapiDoc; use zino_core::{ application::{Application, Plugin, ServerTag}, @@ -86,22 +86,18 @@ impl Application for ActixCluster { ); // Server config - let project_dir = Self::project_dir(); - let default_public_dir = project_dir.join("public"); + let mut public_dir = "public"; let mut public_route_prefix = "/public"; - let mut public_dir = PathBuf::new(); let mut backlog = 2048; // Maximum number of pending connections let mut max_connections = 25000; // Maximum number of concurrent connections let mut body_limit = 128 * 1024 * 1024; // 128MB let mut request_timeout = Duration::from_secs(60); // 60 seconds if let Some(config) = app_state.get_config("server") { if let Some(dir) = config.get_str("page-dir") { + public_dir = dir; public_route_prefix = "/page"; - public_dir.push(dir); } else if let Some(dir) = config.get_str("public-dir") { - public_dir.push(dir); - } else { - public_dir = default_public_dir; + public_dir = dir; } if let Some(route_prefix) = config.get_str("public-route-prefix") { public_route_prefix = route_prefix; @@ -118,10 +114,9 @@ impl Application for ActixCluster { if let Some(timeout) = config.get_duration("request-timeout") { request_timeout = timeout; } - } else { - public_dir = default_public_dir; } + let public_dir = Self::parse_path(public_dir); HttpServer::new(move || { let mut app = App::new().default_service(web::to(|req: Request| async { let res = Response::new(StatusCode::NOT_FOUND); @@ -192,7 +187,7 @@ impl Application for ActixCluster { RapiDoc::with_openapi("/api-docs/openapi.json", Self::openapi()) }; if let Some(custom_html) = config.get_str("custom-html") { - let custom_html_file = project_dir.join(custom_html); + let custom_html_file = Self::parse_path(custom_html); if let Ok(html) = fs::read_to_string(custom_html_file) { rapidoc = rapidoc.custom_html(html); } diff --git a/zino/src/application/axum_cluster.rs b/zino/src/application/axum_cluster.rs index 5b9a4003..ab7afb30 100644 --- a/zino/src/application/axum_cluster.rs +++ b/zino/src/application/axum_cluster.rs @@ -6,9 +6,7 @@ use axum::{ middleware::from_fn, BoxError, Router, }; -use std::{ - any::Any, borrow::Cow, convert::Infallible, fs, net::SocketAddr, path::PathBuf, time::Duration, -}; +use std::{any::Any, borrow::Cow, convert::Infallible, fs, net::SocketAddr, time::Duration}; use tokio::{net::TcpListener, runtime::Builder, signal}; use tower::{ timeout::{error::Elapsed, TimeoutLayer}, @@ -104,21 +102,17 @@ impl Application for AxumCluster { ); // Server config - let project_dir = Self::project_dir(); - let default_public_dir = project_dir.join("public"); + let mut public_dir = "public"; let mut public_route_prefix = "/public"; - let mut public_dir = PathBuf::new(); let mut body_limit = 128 * 1024 * 1024; // 128MB let mut request_timeout = Duration::from_secs(60); // 60 seconds let mut keep_alive_timeout = 75; // 75 seconds if let Some(config) = app_state.get_config("server") { if let Some(dir) = config.get_str("page-dir") { + public_dir = dir; public_route_prefix = "/page"; - public_dir.push(dir); } else if let Some(dir) = config.get_str("public-dir") { - public_dir.push(dir); - } else { - public_dir = default_public_dir; + public_dir = dir; } if let Some(route_prefix) = config.get_str("public-route-prefix") { public_route_prefix = route_prefix; @@ -132,11 +126,10 @@ impl Application for AxumCluster { if let Some(timeout) = config.get_duration("keep-alive-timeout") { keep_alive_timeout = timeout.as_secs(); } - } else { - public_dir = default_public_dir; } let mut app = Router::new(); + let public_dir = Self::parse_path(public_dir); if public_dir.exists() { let index_file = public_dir.join("index.html"); let favicon_file = public_dir.join("favicon.ico"); @@ -193,7 +186,7 @@ impl Application for AxumCluster { RapiDoc::with_openapi("/api-docs/openapi.json", Self::openapi()) }; if let Some(custom_html) = config.get_str("custom-html") { - let custom_html_file = project_dir.join(custom_html); + let custom_html_file = Self::parse_path(custom_html); if let Ok(html) = fs::read_to_string(custom_html_file) { app = app.merge(rapidoc.custom_html(html).path(path)); } else { diff --git a/zino/src/application/dioxus_desktop.rs b/zino/src/application/dioxus_desktop.rs index 3f657eef..bd1245a8 100644 --- a/zino/src/application/dioxus_desktop.rs +++ b/zino/src/application/dioxus_desktop.rs @@ -76,7 +76,6 @@ where let app_name = Self::name(); let app_version = Self::version(); let app_state = Self::shared_state(); - let project_dir = Self::project_dir(); let in_prod_mode = app_env.is_prod(); // Window configuration @@ -136,7 +135,7 @@ where custom_heads.push(r#""#.to_owned()); let icon = config.get_str("icon").unwrap_or("public/favicon.ico"); - let icon_file = project_dir.join(icon); + let icon_file = Self::parse_path(icon); if icon_file.exists() { match ImageReader::open(&icon_file) .map_err(ImageError::IoError) @@ -179,13 +178,13 @@ where desktop_config = desktop_config.with_custom_head(custom_heads.join("\n")); if let Some(dir) = config.get_str("resource-dir") { - desktop_config = desktop_config.with_resource_directory(project_dir.join(dir)); + desktop_config = desktop_config.with_resource_directory(Self::parse_path(dir)); } if let Some(dir) = config.get_str("data-dir") { - desktop_config = desktop_config.with_data_directory(project_dir.join(dir)); + desktop_config = desktop_config.with_data_directory(Self::parse_path(dir)); } if let Some(custom_index) = config.get_str("custom-index") { - let index_file = project_dir.join(custom_index); + let index_file = Self::parse_path(custom_index); match fs::read_to_string(&index_file) { Ok(custom_index) => { desktop_config = desktop_config.with_custom_index(custom_index); diff --git a/zino/src/application/ntex_cluster.rs b/zino/src/application/ntex_cluster.rs index 7d311799..04dd06fe 100644 --- a/zino/src/application/ntex_cluster.rs +++ b/zino/src/application/ntex_cluster.rs @@ -10,7 +10,6 @@ use ntex::{ }, }; use ntex_files::{Files, NamedFile}; -use std::path::PathBuf; use zino_core::{ application::{Application, Plugin, ServerTag}, extension::TomlTableExt, @@ -89,22 +88,18 @@ impl Application for NtexCluster { ); // Server config - let project_dir = Self::project_dir(); - let default_public_dir = project_dir.join("public"); + let mut public_dir = "public"; let mut public_route_prefix = "/public"; - let mut public_dir = PathBuf::new(); let mut backlog = 2048; // Maximum number of pending connections let mut max_connections = 25000; // Maximum number of concurrent connections let mut body_limit = 128 * 1024 * 1024; // 128MB let mut request_timeout = 60; // 60 seconds if let Some(config) = app_state.get_config("server") { if let Some(dir) = config.get_str("page-dir") { + public_dir = dir; public_route_prefix = "/page"; - public_dir.push(dir); } else if let Some(dir) = config.get_str("public-dir") { - public_dir.push(dir); - } else { - public_dir = default_public_dir; + public_dir = dir; } if let Some(route_prefix) = config.get_str("public-route-prefix") { public_route_prefix = route_prefix; @@ -124,10 +119,9 @@ impl Application for NtexCluster { { request_timeout = timeout; } - } else { - public_dir = default_public_dir; } + let public_dir = Self::parse_path(public_dir); HttpServer::new(move || { let mut app = App::new(); if public_dir.exists() {