diff --git a/README_BOOK_CATALOG.md b/README_BOOK_CATALOG.md new file mode 100644 index 0000000..825be1b --- /dev/null +++ b/README_BOOK_CATALOG.md @@ -0,0 +1,155 @@ +# Book Catalog Example + +A complete example demonstrating the nginx-test SQLite module with a read-only book catalog. + +## Features + +- **SQLite Database**: Stores book information (title, author, ISBN, year, genre, description, rating) +- **Multiple Views**: Different locations for browsing by category +- **Template Inheritance**: Global templates (header, footer, book_card) shared across all pages +- **Local Templates**: Category-specific styling and layouts +- **Responsive Design**: Modern, gradient-styled UI + +## Setup + +### 1. Create and populate the database + +```bash +chmod +x setup_book_catalog.sh +./setup_book_catalog.sh +``` + +This creates `book_catalog.db` with 10 sample technical books across three genres: +- Programming +- Databases +- Computer Science + +### 2. Build the module + +```bash +direnv exec "$PWD" cargo build +``` + +### 3. Start nginx + +```bash +./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_catalog.conf -p . +``` + +### 4. Visit the catalog + +Open your browser to: +- http://localhost:8080/ (redirects to all books) +- http://localhost:8080/books/all +- http://localhost:8080/books/programming +- http://localhost:8080/books/databases +- http://localhost:8080/books/computer-science + +### 5. Stop nginx + +```bash +./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_catalog.conf -p . +``` + +## Directory Structure + +``` +nginx-test/ +├── book_catalog.db # SQLite database +├── setup_book_catalog.sh # Database setup script +├── conf/ +│ └── book_catalog.conf # Nginx configuration +└── server_root/ + ├── global_templates/ # Shared templates + │ ├── header.hbs # Page header with navigation + │ ├── footer.hbs # Page footer + │ └── book_card.hbs # Reusable book card partial + └── books/ + ├── all/ + │ └── list.hbs # All books page + ├── programming/ + │ └── list.hbs # Programming books page + ├── databases/ + │ └── list.hbs # Database books page + └── computer-science/ + └── list.hbs # CS books page +``` + +## How It Works + +### Template Loading Order + +For each request, the module: + +1. **Loads global templates** from `server_root/global_templates/`: + - `header.hbs` - Page structure and navigation + - `footer.hbs` - Page footer + - `book_card.hbs` - Book display component + +2. **Loads local templates** from the location's directory: + - Each category has its own `list.hbs` with custom styling + - Local templates can override global ones + +3. **Renders the main template** with SQL query results + +### Template Usage + +**In list.hbs:** +```handlebars +{{> header}} + +
+ {{#each results}} + {{> book_card}} + {{/each}} +
+ +{{> footer}} +``` + +The `{{> header}}`, `{{> book_card}}`, and `{{> footer}}` are partials loaded from the global templates directory. + +### SQL Queries + +Each location runs a different SQL query: + +- **All books**: `SELECT * FROM books ORDER BY rating DESC, title` +- **Programming**: `SELECT * FROM books WHERE genre = 'Programming' ...` +- **Databases**: `SELECT * FROM books WHERE genre = 'Databases' ...` +- **Computer Science**: `SELECT * FROM books WHERE genre = 'Computer Science' ...` + +Results are passed to the template as a `results` array. + +## Customization + +### Adding More Books + +```bash +sqlite3 book_catalog.db +``` + +```sql +INSERT INTO books (title, author, isbn, year, genre, description, rating) +VALUES ('Your Book', 'Author Name', '978-XXXXXXXXXX', 2024, 'Programming', 'Description here', 4.5); +``` + +### Adding New Categories + +1. Create a new genre in the database +2. Add a location block in `book_catalog.conf` +3. Create a template directory under `server_root/books/` +4. Add the category to the navigation in `header.hbs` + +### Styling + +Each category's `list.hbs` contains embedded CSS. Modify the ` + +
+
+

📚

+

Book Collection

+
+
+

+

Top Rated

+
+
+

🔍

+

Browse All

+
+
+ +

All Books in Collection

+ +
+ {{#each results}} + {{> book_card}} + {{/each}} +
+ +{{> footer}} + diff --git a/server_root/books/computer-science/list.hbs b/server_root/books/computer-science/list.hbs new file mode 100644 index 0000000..0e686b8 --- /dev/null +++ b/server_root/books/computer-science/list.hbs @@ -0,0 +1,107 @@ +{{> header}} + + + +
+

🎓 Computer Science Books

+

Fundamental concepts, algorithms, and theoretical computer science

+
+ +
+ {{#each results}} + {{> book_card}} + {{/each}} +
+ +{{#unless results}} +

No computer science books found.

+{{/unless}} + +{{> footer}} + diff --git a/server_root/books/databases/list.hbs b/server_root/books/databases/list.hbs new file mode 100644 index 0000000..a8aad8b --- /dev/null +++ b/server_root/books/databases/list.hbs @@ -0,0 +1,107 @@ +{{> header}} + + + +
+

🗄️ Database Books

+

Deep dive into database systems, design, and data management

+
+ +
+ {{#each results}} + {{> book_card}} + {{/each}} +
+ +{{#unless results}} +

No database books found.

+{{/unless}} + +{{> footer}} + diff --git a/server_root/books/programming/list.hbs b/server_root/books/programming/list.hbs new file mode 100644 index 0000000..4c6ce74 --- /dev/null +++ b/server_root/books/programming/list.hbs @@ -0,0 +1,107 @@ +{{> header}} + + + +
+

💻 Programming Books

+

Books focused on programming languages, practices, and software development

+
+ +
+ {{#each results}} + {{> book_card}} + {{/each}} +
+ +{{#unless results}} +

No programming books found.

+{{/unless}} + +{{> footer}} + diff --git a/server_root/global_templates/book_card.hbs b/server_root/global_templates/book_card.hbs new file mode 100644 index 0000000..fcb32de --- /dev/null +++ b/server_root/global_templates/book_card.hbs @@ -0,0 +1,16 @@ +
+
+

{{title}}

+
⭐ {{rating}}
+
+

by {{author}}

+

{{description}}

+
+ {{genre}} + {{year}} + {{#if isbn}} + ISBN: {{isbn}} + {{/if}} +
+
+ diff --git a/server_root/global_templates/footer.hbs b/server_root/global_templates/footer.hbs new file mode 100644 index 0000000..3a679b5 --- /dev/null +++ b/server_root/global_templates/footer.hbs @@ -0,0 +1,9 @@ + + + + + + diff --git a/server_root/global_templates/header.hbs b/server_root/global_templates/header.hbs new file mode 100644 index 0000000..587eeaf --- /dev/null +++ b/server_root/global_templates/header.hbs @@ -0,0 +1,77 @@ + + + + + + Book Catalog + + + +
+
+

📚 Book Catalog

+

Explore our collection of technical books

+
+ +
+ diff --git a/person.hbs b/server_root/people/person.hbs similarity index 100% rename from person.hbs rename to server_root/people/person.hbs diff --git a/setup_book_catalog.sh b/setup_book_catalog.sh new file mode 100755 index 0000000..146a1e3 --- /dev/null +++ b/setup_book_catalog.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Setup script for book catalog example + +set -e + +echo "Setting up book catalog database..." + +# Create the database +rm -f book_catalog.db +sqlite3 book_catalog.db < ngx_int_t { - unsafe { - let htcf = - NgxHttpCoreModule::main_conf_mut(&*cf).expect("failed to get core main conf"); - - let h = ngx_array_push( - &mut htcf.phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers, - ) as *mut ngx_http_handler_pt; - if h.is_null() { - return core::Status::NGX_ERROR.into(); - } - - // set an Access phase handler - *h = Some(howto_access_handler); - core::Status::NGX_OK.into() - } + unsafe extern "C" fn postconfiguration(_cf: *mut ngx_conf_t) -> ngx_int_t { + core::Status::NGX_OK.into() } } @@ -49,6 +35,11 @@ unsafe impl HttpModuleLocationConf for Module { type LocationConf = ModuleConfig; } +// Implement HttpModuleMainConf to define our global configuration +unsafe impl HttpModuleMainConf for Module { + type MainConf = MainConfig; +} + // Create a ModuleConfig to save our configuration state. #[derive(Debug, Default)] struct ModuleConfig { @@ -57,6 +48,21 @@ struct ModuleConfig { template_path: String, } +// Global configuration for shared templates +#[derive(Debug, Default)] +struct MainConfig { + global_templates_dir: String, +} + +impl http::Merge for MainConfig { + fn merge(&mut self, prev: &MainConfig) -> Result<(), MergeConfigError> { + if self.global_templates_dir.is_empty() { + self.global_templates_dir = prev.global_templates_dir.clone(); + } + Ok(()) + } +} + // Implement our Merge trait to merge configuration with higher layers. impl http::Merge for ModuleConfig { fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> { @@ -83,8 +89,8 @@ impl http::Merge for ModuleConfig { static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { preconfiguration: Some(Module::preconfiguration), postconfiguration: Some(Module::postconfiguration), - create_main_conf: None, - init_main_conf: None, + create_main_conf: Some(Module::create_main_conf), + init_main_conf: Some(Module::init_main_conf), create_srv_conf: None, merge_srv_conf: None, create_loc_conf: Some(Module::create_loc_conf), @@ -133,7 +139,15 @@ pub static mut ngx_http_howto_module: ngx_module_t = ngx_module_t { // sure to terminate the array with an empty command. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -static mut ngx_http_howto_commands: [ngx_command_t; 4] = [ +static mut ngx_http_howto_commands: [ngx_command_t; 5] = [ + ngx_command_t { + name: ngx_string!("sqlite_global_templates"), + type_: (NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1) as ngx_uint_t, + set: Some(ngx_http_howto_commands_set_global_templates), + conf: 0, // Main conf offset + offset: 0, + post: std::ptr::null_mut(), + }, ngx_command_t { name: ngx_string!("sqlite_db"), type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, @@ -171,6 +185,21 @@ static mut ngx_http_howto_commands: [ngx_command_t; 4] = [ }, ]; +#[unsafe(no_mangle)] +extern "C" fn ngx_http_howto_commands_set_global_templates( + cf: *mut ngx_conf_t, + _cmd: *mut ngx_command_t, + conf: *mut c_void, +) -> *mut c_char { + unsafe { + let conf = &mut *(conf as *mut MainConfig); + let args = (*(*cf).args).elts as *mut ngx_str_t; + conf.global_templates_dir = (*args.add(1)).to_string(); + }; + + std::ptr::null_mut() +} + #[unsafe(no_mangle)] extern "C" fn ngx_http_howto_commands_set_db_path( cf: *mut ngx_conf_t, @@ -211,11 +240,51 @@ extern "C" fn ngx_http_howto_commands_set_template_path( let conf = &mut *(conf as *mut ModuleConfig); let args = (*(*cf).args).elts as *mut ngx_str_t; conf.template_path = (*args.add(1)).to_string(); + + // Set the content handler for this location + let clcf = NgxHttpCoreModule::location_conf_mut(&*cf) + .expect("failed to get core location conf"); + clcf.handler = Some(howto_access_handler); }; std::ptr::null_mut() } +// Load all .hbs templates from a directory into the Handlebars registry +fn load_templates_from_dir(reg: &mut Handlebars, dir_path: &str) -> std::io::Result { + use std::fs; + use std::path::Path; + + let dir = Path::new(dir_path); + if !dir.exists() || !dir.is_dir() { + return Ok(0); + } + + let mut count = 0; + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + if let Some(ext) = path.extension() { + if ext == "hbs" { + if let Some(stem) = path.file_stem() { + if let Some(name) = stem.to_str() { + if let Err(e) = reg.register_template_file(name, &path) { + eprintln!("Failed to register template {}: {}", path.display(), e); + } else { + count += 1; + } + } + } + } + } + } + } + + Ok(count) +} + // Execute a generic SQL query and return results as JSON-compatible data fn execute_query(db_path: &str, query: &str) -> Result>> { let conn = Connection::open(db_path)?; @@ -271,6 +340,33 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| { ngx_log_debug_http!(request, "sqlite module handler called"); + // Resolve template path relative to document root and location + let core_loc_conf = + NgxHttpCoreModule::location_conf(request).expect("failed to get core location conf"); + let doc_root = match (*core_loc_conf).root.to_str() { + Ok(s) => s, + Err(e) => { + ngx_log_debug_http!(request, "failed to decode root path: {}", e); + return http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); + } + }; + let uri = match request.path().to_str() { + Ok(s) => s, + Err(e) => { + ngx_log_debug_http!(request, "failed to decode URI path: {}", e); + return http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); + } + }; + let template_full_path = format!("{}{}/{}", doc_root, uri, co.template_path); + + ngx_log_debug_http!(request, "resolved template path: {}", template_full_path); + + // Get the directory containing the main template for local templates + let template_dir = std::path::Path::new(&template_full_path) + .parent() + .and_then(|p| p.to_str()) + .unwrap_or(""); + // Execute the configured SQL query let results = match execute_query(&co.db_path, &co.query) { Ok(results) => results, @@ -280,12 +376,39 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| { } }; - // Setup Handlebars and register the configured template + // Setup Handlebars and load templates let mut reg = Handlebars::new(); - match reg.register_template_file("template", &co.template_path) { - Ok(_) => (), + + // First, load global templates if configured + let main_conf = Module::main_conf(request).expect("main config is none"); + if !main_conf.global_templates_dir.is_empty() { + match load_templates_from_dir(&mut reg, &main_conf.global_templates_dir) { + Ok(count) => { + ngx_log_debug_http!(request, "loaded {} global templates from {}", count, main_conf.global_templates_dir); + } + Err(e) => { + ngx_log_debug_http!(request, "warning: failed to load global templates: {}", e); + } + } + } + + // Then, load local templates (these override global ones) + match load_templates_from_dir(&mut reg, template_dir) { + Ok(count) => { + ngx_log_debug_http!(request, "loaded {} local templates from {}", count, template_dir); + } Err(e) => { - ngx_log_debug_http!(request, "failed to load template: {}", e); + ngx_log_debug_http!(request, "warning: failed to load local templates: {}", e); + } + } + + // Finally, register the main template (overriding if it was loaded from directories) + match reg.register_template_file("template", &template_full_path) { + Ok(_) => { + ngx_log_debug_http!(request, "registered main template: {}", template_full_path); + }, + Err(e) => { + ngx_log_debug_http!(request, "failed to load main template: {}", e); return http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); } } diff --git a/start_book_catalog.sh b/start_book_catalog.sh new file mode 100755 index 0000000..5b2ef2a --- /dev/null +++ b/start_book_catalog.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Quick start script for the book catalog example + +set -e + +echo "📚 Starting Book Catalog..." +echo "" + +# Check if database exists +if [ ! -f "book_catalog.db" ]; then + echo "Database not found. Running setup..." + ./setup_book_catalog.sh + echo "" +fi + +# Check if module is built +if [ ! -f "target/debug/libnginx_test.dylib" ]; then + echo "Module not built. Building..." + direnv exec "$PWD" cargo build + echo "" +fi + +# Start nginx +echo "Starting nginx on http://localhost:8080" +./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_catalog.conf -p . + +echo "" +echo "✅ Book Catalog is running!" +echo "" +echo "Visit:" +echo " • http://localhost:8080/books/all - All books" +echo " • http://localhost:8080/books/programming - Programming books" +echo " • http://localhost:8080/books/databases - Database books" +echo " • http://localhost:8080/books/computer-science - CS books" +echo "" +echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_catalog.conf -p ." +