From 87d1b33d7c48dbd192b3967310f6e5eee17cabe3 Mon Sep 17 00:00:00 2001
From: Josef Rokos <pepa@bukova.info>
Date: Fri, 18 Apr 2025 11:57:40 +0200
Subject: [PATCH] Application can now run as Docker container.

---
 Cargo.lock                       |  1 +
 Cargo.toml                       |  1 +
 Dockerfile                       | 51 ++++++++++++++++++++++++++++++++
 migrations/05_dyn_style_name.sql |  1 +
 src/backend/appearance.rs        | 38 ++++++++++++++++++++----
 src/backend/data.rs              |  3 +-
 src/components/header.rs         | 12 ++++++--
 src/main.rs                      |  2 +-
 8 files changed, 99 insertions(+), 10 deletions(-)
 create mode 100644 Dockerfile
 create mode 100644 migrations/05_dyn_style_name.sql

diff --git a/Cargo.lock b/Cargo.lock
index 1105fa6..56d24d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3235,6 +3235,7 @@ dependencies = [
  "lettre",
  "log",
  "pwhash",
+ "rand",
  "regex",
  "rust_decimal",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index fa0df67..7a1f55a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,6 +41,7 @@ leptos-captcha = "0.2.0"
 charts-rs = { version = "0.3.5", optional = true}
 #image = { version = "0.24.8", optional = true }
 base64 = "0.22.0"
+rand = "0.8.5"
 
 [features]
 csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..665f95b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,51 @@
+FROM rust:1.86.0-bookworm AS builder
+
+# Install cargo-binstall, which makes it easier to install other
+# cargo extensions like cargo-leptos
+RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
+RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
+RUN cp cargo-binstall /usr/local/cargo/bin
+
+# Install required tools
+RUN apt-get update -y \
+  && apt-get install -y --no-install-recommends clang
+
+# Install cargo-leptos
+RUN cargo binstall cargo-leptos -y
+
+RUN rustup default stable
+
+# Add the WASM target
+RUN rustup target add wasm32-unknown-unknown
+#RUN rustup target add wasm32-unknown-unknown --toolchain nightly
+
+
+# Make an /app dir, which everything will eventually live in
+RUN mkdir -p /app
+WORKDIR /app
+COPY . .
+
+# Build the app
+#RUN cargo leptos build --release -vv
+RUN LEPTOS_OUTPUT_NAME="rezervator-$(tr -dc a-z0-9 </dev/urandom | head -c 10)" cargo leptos build -r -P
+
+FROM debian:bookworm-slim AS runtime
+WORKDIR /app
+RUN apt-get update -y \
+  && apt-get install -y --no-install-recommends openssl ca-certificates \
+  && apt-get autoremove -y \
+  && apt-get clean -y \
+  && rm -rf /var/lib/apt/lists/*
+
+# Copy the server binary to the /app directory
+COPY --from=builder /app/target/release/rezervator /app/
+
+# /target/site contains our JS/WASM/CSS, etc.
+COPY --from=builder /app/target/site /app/target/site
+
+# Set any required env variables and
+EXPOSE 3000
+
+# -- NB: update binary name from "leptos_start" to match your app name in Cargo.toml --
+# Run the server
+CMD ["/app/rezervator", "-c /app/target/site/data/config.toml"]
\ No newline at end of file
diff --git a/migrations/05_dyn_style_name.sql b/migrations/05_dyn_style_name.sql
new file mode 100644
index 0000000..843f4de
--- /dev/null
+++ b/migrations/05_dyn_style_name.sql
@@ -0,0 +1 @@
+ALTER TABLE appearance ADD css_name VARCHAR;
\ No newline at end of file
diff --git a/src/backend/appearance.rs b/src/backend/appearance.rs
index ef7c400..36cff49 100644
--- a/src/backend/appearance.rs
+++ b/src/backend/appearance.rs
@@ -6,6 +6,7 @@ use crate::components::data_form::ForValidation;
 
 cfg_if! { if #[cfg(feature = "ssr")] {
 
+    use std::fs;
     use actix_web::{post, Responder};
     use actix_multipart::Multipart;
     use actix_session::Session;
@@ -20,6 +21,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
     use actix_web::web::Data;
     use crate::backend::AppData;
     use regex::Regex;
+    use rand::Rng;
+    use rand::distributions::Alphanumeric;
 
     pub async fn check_appearance(pool: &PgPool) -> Result<(), AppError> {
         let count: (i64,) = query_as("SELECT COUNT(id) FROM appearance")
@@ -32,6 +35,11 @@ cfg_if! { if #[cfg(feature = "ssr")] {
                 .await?;
         }
 
+        let app = query_as::<_, Appearance>("SELECT * FROM appearance").fetch_one(pool).await?;
+        if let None = app.css_name {
+            query("UPDATE appearance SET css_name = 'banner.css'").execute(pool).await?;
+        }
+
         Ok(())
     }
 
@@ -44,8 +52,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
         Ok(())
     }
 
-    async fn modify_style(file_name: &str) -> Result<(), AppError> {
-        let mut css_file = File::open("target/site/banner.css")?;
+    async fn modify_style(file_name: &str, pool: &PgPool) -> Result<(), AppError> {
+        let mut css_file = File::open("target/site/data/banner.css")?;
         let mut css_str= String::new();
         css_file.read_to_string(&mut css_str)?;
 
@@ -56,8 +64,26 @@ cfg_if! { if #[cfg(feature = "ssr")] {
             css_str = re.replace(&css_str, &format!("background-image: url('{}')", file_name)).to_string();
         }
 
-        let mut css_file = File::create("target/site/banner.css")?;
+        let old_css: (String,) = query_as("SELECT css_name FROM appearance")
+            .fetch_one(pool)
+            .await?;
+
+        if old_css.0 != "banner.css" {
+            fs::remove_file(format!("target/site/data/{}", old_css.0))?;
+        }
+
+        let s: String = rand::thread_rng()
+            .sample_iter(&Alphanumeric)
+            .take(5)
+            .map(char::from)
+            .collect();
+        let css_name = format!("banner-{}.css", s);
+        let mut css_file = File::create(format!("target/site/data/{}", css_name))?;
         css_file.write_all(css_str.as_bytes())?;
+        query("UPDATE appearance SET css_name = $1")
+            .bind(css_name)
+            .execute(pool)
+            .await?;
 
         Ok(())
     }
@@ -84,14 +110,14 @@ cfg_if! { if #[cfg(feature = "ssr")] {
                 return Redirect::to("/admin/appearance").see_other();
             }
 
-            let mut file = File::create(format!("target/site/{}", file_name)).unwrap();
+            let mut file = File::create(format!("target/site/data/{}", file_name)).unwrap();
             let _name = field.name();
             while let Some(chunk) = field.next().await {
                 let c = chunk.unwrap();
                 let _ = file.write_all(&c);
             }
             let _ = set_banner_name(&file_name, &app_data.db_pool).await;
-            let _ = modify_style(&file_name).await;
+            let _ = modify_style(&file_name, &app_data.db_pool).await;
         }
 
         Redirect::to("/admin/appearance").see_other()
@@ -149,7 +175,7 @@ pub async fn delete_banner() -> Result<ApiResponse<()>, ServerFnError> {
         .await?;
 
     if let Some(f) = appearance.banner {
-        fs::remove_file(format!("target/site/{}", f))?;
+        fs::remove_file(format!("target/site/data/{}", f))?;
     }
 
     Ok(ApiResponse::Data(()))
diff --git a/src/backend/data.rs b/src/backend/data.rs
index 54e1e50..b6da0af 100644
--- a/src/backend/data.rs
+++ b/src/backend/data.rs
@@ -719,7 +719,8 @@ pub struct Appearance {
     id: i32,
     pub banner: Option<String>,
     pub text: Option<String>,
-    pub title: Option<String>
+    pub title: Option<String>,
+    pub css_name: Option<String>
 }
 
 impl Appearance {
diff --git a/src/components/header.rs b/src/components/header.rs
index 5640270..37db58b 100644
--- a/src/components/header.rs
+++ b/src/components/header.rs
@@ -1,13 +1,14 @@
 use leptos::*;
 use leptos_meta::*;
 use crate::app::DialogHelper;
+use crate::backend::appearance::get_appearance;
 use crate::components::user_menu::MenuOpener;
 
 #[component]
 pub fn Header() -> impl IntoView {
     let drawer = use_context::<MenuOpener>().expect("No drawer opener");
     let dlg_helper = use_context::<DialogHelper>().expect("No dialog helper");
-    //let banner_css = create_signal(String::new());
+    let appearance = create_blocking_resource(||(), |_| get_appearance());
 
     view! {
         <Html
@@ -39,7 +40,14 @@ pub fn Header() -> impl IntoView {
         <Link rel="stylesheet" href="/vendor/css/core.css" />
         <Link rel="stylesheet" href="/vendor/css/theme-default.css" />
         <Link rel="stylesheet" href="/css/demo.css" />
-        <Link rel="stylesheet" href="/banner.css" />
+        <Transition fallback=move || view! {""}>
+            {
+                appearance.get().map(|a| match a {
+                    Ok(a) => view! {<Link rel="stylesheet" href={format!("/data/{}", a.css_name.unwrap_or_default())} />},
+                    Err(_) => view! {<Link rel="stylesheet" href="/data/banner.css" />}
+                })
+            }
+        </Transition>
         <Link rel="stylesheet" href="/vendor/css/control.css" />
 
         //<!-- Vendors CSS -->
diff --git a/src/main.rs b/src/main.rs
index 70995d8..5cb9c5a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -43,7 +43,7 @@ async fn main() -> std::io::Result<()> {
     Pow::init_random().expect("Cannot init captcha");
 
     let cfg_path = matches.opt_str("c").unwrap_or("config.toml".to_string());
-    let srv_conf = load_config(&cfg_path);
+    let srv_conf = load_config(cfg_path.trim());
 
     env_logger::Builder::from_env(Env::default().default_filter_or(srv_conf.logging().severity())).init();
     info!("Starting server");