├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── docs └── Template.md ├── include ├── debug.h ├── file_watcher.h ├── html_serve.h ├── request_handler.h ├── server.h ├── socket_utils.h ├── sqlite_handler.h ├── template.h └── websocket.h ├── src ├── file_watcher.c ├── handle_client.c ├── html_serve.c ├── request_handler.c ├── server.c ├── socket_utils.c ├── sqlite_handler.c ├── template.c └── websocket.c ├── template-examples ├── 01-variables.html ├── 02-conditionals.html ├── 03-loops.html ├── 04-conditional-loops.html └── 05-sqlite-queries.html ├── test ├── create_sample_db.sql ├── minimal.html ├── sample.db └── test.html └── www ├── conditional-loops.html ├── conditionals.html ├── index.html ├── loops.html ├── sample.db ├── sql.html └── variables.html /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .vscode -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists.txt 2 | cmake_minimum_required(VERSION 3.10) 3 | project(Blink) 4 | 5 | # Set output directories for binaries and libraries 6 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 7 | 8 | # Find required packages 9 | find_package(OpenSSL REQUIRED) 10 | find_package(SQLite3 REQUIRED) 11 | 12 | # Include directories 13 | include_directories(include ${OPENSSL_INCLUDE_DIR} ${SQLite3_INCLUDE_DIRS}) 14 | 15 | # Add the executable 16 | add_executable(blink 17 | src/server.c 18 | src/socket_utils.c 19 | src/html_serve.c 20 | src/request_handler.c 21 | src/template.c 22 | src/file_watcher.c 23 | src/websocket.c 24 | src/sqlite_handler.c 25 | ) 26 | 27 | # Link with required libraries 28 | target_link_libraries(blink ${OPENSSL_LIBRARIES} ${SQLite3_LIBRARIES} pthread) 29 | 30 | # Copy www directory to build directory 31 | add_custom_command( 32 | TARGET blink POST_BUILD 33 | COMMAND ${CMAKE_COMMAND} -E copy_directory 34 | ${CMAKE_SOURCE_DIR}/www ${CMAKE_BINARY_DIR}/bin/www 35 | COMMENT "Copying www directory to build directory" 36 | ) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 dexter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blink: Lightweight Web Server with Advanced Templating 2 | 3 | Blink is a lightweight, powerful web server written in C that features a comprehensive templating system with support for dynamic content, conditional logic, loops, and SQLite database integration. It's designed to be fast, easy to use, and perfect for both development and small-scale deployments. 4 | 5 | [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-Support-yellow.svg)](https://buymeacoffee.com/trish07) 6 | 7 | ## Features 8 | 9 | - **Lightweight HTTP Server**: Fast and efficient C-based HTTP server with minimal dependencies 10 | - **Hot Reloading**: Automatic browser refresh when HTML files are modified 11 | - **WebSocket Support**: Real-time bidirectional communication 12 | - **Comprehensive Templating System**: 13 | - Variable replacement 14 | - Conditional logic (if/else blocks) 15 | - Loops with item iteration 16 | - Conditional loops with filtering 17 | - Nested template structures 18 | - **SQLite Integration**: 19 | - Execute SQL queries directly in templates 20 | - Display query results as formatted HTML tables 21 | - Form-based database operations (Create, Read, Update, Delete) 22 | - Placeholder substitution for safe user input handling 23 | - **Flexible Configuration**: 24 | - Customizable port settings 25 | - Custom HTML file serving 26 | - Database path configuration 27 | - Template processing toggles 28 | 29 | ## Project Structure 30 | 31 | ``` 32 | blink/ 33 | ├── CMakeLists.txt # Build configuration 34 | ├── LICENSE # Project license 35 | ├── README.md # Project documentation 36 | ├── .gitignore # Git ignore file 37 | │ 38 | ├── include/ # Header files 39 | │ ├── blink_orm.h # ORM functionality for SQLite 40 | │ ├── debug.h # Debugging utilities 41 | │ ├── file_watcher.h # File watching for hot reload 42 | │ ├── html_serve.h # HTML serving functionality 43 | │ ├── request_handler.h # HTTP request handler 44 | │ ├── server.h # Main server header 45 | │ ├── socket_utils.h # Socket utilities 46 | │ ├── sqlite_handler.h # SQLite database integration 47 | │ ├── template.h # Template processing 48 | │ └── websocket.h # WebSocket protocol support 49 | │ 50 | ├── src/ # Source code files 51 | │ ├── file_watcher.c # Implementation of file watcher 52 | │ ├── handle_client.c # Client connection handler 53 | │ ├── html_serve.c # HTML content serving 54 | │ ├── request_handler.c # HTTP request processing 55 | │ ├── server.c # Main server implementation 56 | │ ├── socket_utils.c # Socket utility functions 57 | │ ├── sqlite_handler.c # SQLite database functions 58 | │ ├── template.c # Template engine implementation 59 | │ └── websocket.c # WebSocket implementation 60 | │ 61 | └── build/ # Build directory (generated) 62 | └── bin/ # Compiled binaries 63 | └── blink # Main executable 64 | ``` 65 | 66 | ## Quick Start 67 | 68 | ### Prerequisites 69 | 70 | - **CMake** (version 3.10 or higher) 71 | - **GCC** or another compatible C compiler 72 | - **OpenSSL** development libraries 73 | - **SQLite3** development libraries 74 | - **Linux** or **WSL** (Windows Subsystem for Linux) recommended 75 | 76 | ### Installation 77 | 78 | ```bash 79 | # Install dependencies (Debian/Ubuntu) 80 | sudo apt update 81 | sudo apt install build-essential cmake libssl-dev libsqlite3-dev 82 | 83 | # Clone the repository 84 | git clone https://github.com/dexter-xD/blink.git 85 | cd blink 86 | 87 | # Build the project 88 | mkdir build && cd build 89 | cmake .. 90 | make 91 | 92 | # Run the server 93 | ./bin/blink 94 | ``` 95 | 96 | ### Command-Line Options 97 | 98 | ``` 99 | Options: 100 | -p, --port PORT Specify port number (default: 8080) 101 | -s, --serve FILE Specify a custom HTML file to serve 102 | -db, --database FILE Specify SQLite database path 103 | -n, --no-templates Disable template processing 104 | -h, --help Display help message 105 | ``` 106 | 107 | Example usage: 108 | 109 | ```bash 110 | # Run with a custom HTML file and SQLite database 111 | ./bin/blink --serve myapp.html --database mydata.db --port 9000 112 | ``` 113 | 114 | ## Template Engine Guide 115 | 116 | The Blink template engine allows dynamic HTML generation with various powerful features. Here's an overview of the main capabilities: 117 | 118 | ### 1. Variable Replacement 119 | 120 | Define variables using HTML comments and reference them with double curly braces: 121 | 122 | ```html 123 | 124 | 125 |

Welcome, {{username}}!

126 |

You are logged into {{company}} systems.

127 | ``` 128 | 129 | ### 2. Conditional Logic 130 | 131 | Use if-else blocks to display content conditionally: 132 | 133 | ```html 134 | 135 | 136 | {% if is_admin %} 137 |
138 |

Admin Controls

139 | 140 |
141 | {% else %} 142 |

You don't have admin privileges.

143 | {% endif %} 144 | ``` 145 | 146 | ### 3. Loops 147 | 148 | Iterate over items using for loops: 149 | 150 | ```html 151 | 152 | 153 | 158 | ``` 159 | 160 | ### 4. Multi-part Items 161 | 162 | Use pipe-delimited values for structured data: 163 | 164 | ```html 165 | 166 | 167 | 168 | 169 | {% for item in items %} 170 | 171 | 172 | 173 | 174 | 175 | {% endfor %} 176 |
ProductCategoryPrice
{{item.0}}{{item.1}}${{item.2}}
177 | ``` 178 | 179 | ### 5. Conditional Loops 180 | 181 | Filter items in loops using conditions: 182 | 183 | ```html 184 | 185 | 186 |

Fruits Only:

187 | 192 | ``` 193 | 194 | ### 6. SQLite Integration 195 | 196 | Execute SQL queries directly in your templates: 197 | 198 | ```html 199 |

User List

200 | {% query "SELECT id, name, email FROM users ORDER BY name LIMIT 10" %} 201 | 202 |

Item Statistics

203 | {% query "SELECT category, COUNT(*) as count, AVG(price) as avg_price FROM products GROUP BY category" %} 204 | ``` 205 | 206 | ### 7. Form-Based Database Operations 207 | 208 | Create forms that perform database operations: 209 | 210 | ```html 211 |
212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |
224 | ``` 225 | 226 | For detailed documentation on all template features, see the [Template Documentation](./docs/Template.md). 227 | 228 | ## Example Applications 229 | 230 | ### 1. Simple Dynamic Page 231 | 232 | ```html 233 | 234 | 235 | 236 | 237 | 238 | {{page_title}} 239 | 240 | 241 |

Hello, {{username}}!

242 |

Welcome to our website.

243 | 244 | 245 | ``` 246 | 247 | ### 2. Data Dashboard with SQLite 248 | 249 | ```html 250 | 251 | 252 | 253 | Sales Dashboard 254 | 255 | 256 |

Sales Dashboard

257 | 258 |

Recent Orders

259 | {% query "SELECT id, customer_name, amount, date FROM orders ORDER BY date DESC LIMIT 5" %} 260 | 261 |

Sales by Category

262 | {% query "SELECT category, SUM(amount) as total FROM orders GROUP BY category ORDER BY total DESC" %} 263 | 264 | 265 | ``` 266 | 267 | ## Hot Reload Feature 268 | 269 | Blink's hot reload feature automatically refreshes connected browsers when HTML files are modified: 270 | 271 | 1. Start the server with default options 272 | 2. Edit any HTML file in your project directory 273 | 3. The browser will automatically refresh to show your changes 274 | 275 | This feature works by: 276 | - Watching file system events in the HTML directory 277 | - Using WebSockets to notify connected clients 278 | - Injecting a small JavaScript snippet into served HTML pages 279 | 280 | ## WebSocket Support 281 | 282 | Blink includes WebSocket support for real-time bidirectional communication: 283 | 284 | 1. Access WebSocket functionality at `/ws` endpoint 285 | 2. Establish a WebSocket connection from your client-side JavaScript 286 | 3. Exchange messages between client and server in real time 287 | 288 | ## SQLite Database Integration 289 | 290 | To use SQLite features: 291 | 292 | 1. Start Blink with a database: `./bin/blink --database mydata.db` 293 | 2. Use `{% query "SQL_STATEMENT" %}` tags in your HTML templates 294 | 3. Create forms with action="/sql" to perform database operations 295 | 296 | ## Contributing 297 | 298 | Contributions are welcome! Please feel free to submit a Pull Request. 299 | 300 | ## Support Development 301 | 302 | If you find Blink useful, consider supporting its development: 303 | 304 | [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-Support-yellow.svg)](https://www.buymeacoffee.com/yourusername) 305 | 306 | ## License 307 | 308 | This project is licensed under the MIT License - see the LICENSE file for details. 309 | 310 | --- 311 | 312 | ## Template Engine Documentation 313 | 314 | For full documentation on the template engine, see [docs/Template.md](./docs/Template.md). -------------------------------------------------------------------------------- /docs/Template.md: -------------------------------------------------------------------------------- 1 | # Blink Template Engine Documentation 2 | 3 | This document provides a comprehensive guide to using the Blink template engine for dynamic HTML content generation. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Introduction](#introduction) 8 | 2. [Basic Template Syntax](#basic-template-syntax) 9 | 3. [Template Variables](#template-variables) 10 | 4. [Conditional Logic](#conditional-logic) 11 | 5. [Loops](#loops) 12 | 6. [Conditional Loops](#conditional-loops) 13 | 7. [SQLite Queries](#sqlite-queries) 14 | 8. [Nested Elements](#nested-elements) 15 | 9. [Best Practices](#best-practices) 16 | 17 | ## Introduction 18 | 19 | The Blink template engine allows for dynamic HTML generation by combining static HTML with dynamic content. It supports: 20 | 21 | - Variable replacement 22 | - Conditional blocks (if/else) 23 | - Loops over items 24 | - Conditional loops with filtering 25 | - SQLite database queries 26 | - Part extraction from delimited values 27 | 28 | ## Basic Template Syntax 29 | 30 | ### Variable Placeholders 31 | 32 | Variables are referenced using double curly braces: 33 | 34 | ```html 35 |

Hello, {{username}}!

36 | ``` 37 | 38 | ### Conditionals 39 | 40 | Conditional blocks use the following syntax: 41 | 42 | ```html 43 | {% if condition %} 44 | 45 | {% else %} 46 | 47 | {% endif %} 48 | ``` 49 | 50 | ### Loops 51 | 52 | Loops use the following syntax: 53 | 54 | ```html 55 | {% for item in items %} 56 | 57 | {% endfor %} 58 | ``` 59 | 60 | ### SQLite Queries 61 | 62 | SQL queries use the following syntax: 63 | 64 | ```html 65 | {% query "SELECT column1, column2 FROM table_name WHERE condition" %} 66 | ``` 67 | 68 | ### Conditional Loops 69 | 70 | Loops with conditions use the following syntax: 71 | 72 | ```html 73 | {% for item in items if item.1 == "value" %} 74 | 75 | {% endfor %} 76 | ``` 77 | 78 | ## Template Variables 79 | 80 | ### Defining Variables 81 | 82 | Variables can be defined in HTML comments: 83 | 84 | ```html 85 | 86 | ``` 87 | 88 | ### Using Variables 89 | 90 | Variables are accessed using double curly braces: 91 | 92 | ```html 93 | {{page_title}} 94 |

Welcome, {{username}}!

95 | ``` 96 | 97 | ### Multi-part Variables 98 | 99 | Variables can contain delimited values (using the pipe character): 100 | 101 | ```html 102 | 103 | ``` 104 | 105 | **Note**: Accessing parts of regular variables directly (like `{{product.0}}`) is not supported. Use loops for part extraction. 106 | 107 | ## Conditional Logic 108 | 109 | ### Basic Conditions 110 | 111 | ```html 112 | {% if is_admin %} 113 | Admin Panel 114 | {% endif %} 115 | ``` 116 | 117 | ### If/Else Blocks 118 | 119 | ```html 120 | {% if is_logged_in %} 121 |

Welcome back, {{username}}!

122 | {% else %} 123 |

Please log in to continue.

124 | {% endif %} 125 | ``` 126 | 127 | ### Condition Values 128 | 129 | The engine treats the following values as "true": 130 | - "1", "true", "yes", "y", "on" 131 | - Any non-empty string not explicitly false 132 | 133 | The following values are treated as "false": 134 | - "0", "false", "no", "n", "off" 135 | - Empty strings 136 | 137 | ## Loops 138 | 139 | ### Defining Loop Items 140 | 141 | Loop items are defined in HTML comments: 142 | 143 | ```html 144 | 145 | ``` 146 | 147 | ### Basic Loop 148 | 149 | ```html 150 | 155 | ``` 156 | 157 | ### Multi-part Items 158 | 159 | Items can contain parts separated by the pipe character: 160 | 161 | ```html 162 | 163 | ``` 164 | 165 | Access parts using dot notation: 166 | 167 | ```html 168 | 173 | ``` 174 | 175 | ## Conditional Loops 176 | 177 | ### Filtering by Exact Match 178 | 179 | ```html 180 | 185 | ``` 186 | 187 | ### Filtering by Inequality 188 | 189 | ```html 190 | 195 | ``` 196 | 197 | ### Multiple Conditions 198 | 199 | Currently only single conditions are supported. For complex filtering, pre-filter your data. 200 | 201 | ## SQLite Queries 202 | 203 | ### Setting Up SQLite 204 | 205 | To use SQLite queries in your templates, you must start the server with a valid SQLite database using the `-db` or `--database` command-line option: 206 | 207 | ```bash 208 | ./bin/blink --database path/to/your/database.db 209 | ``` 210 | 211 | If you don't provide a database path, but still want to use SQLite features, Blink will create a default database in the current directory named `blink.db`. 212 | 213 | ### Basic Query Syntax 214 | 215 | Execute SQL queries directly in your templates using the query tag: 216 | 217 | ```html 218 | {% query "SELECT * FROM users LIMIT 10" %} 219 | ``` 220 | 221 | The query results will be automatically rendered as an HTML table with the class `sql-table`. 222 | 223 | ### Query Examples 224 | 225 | #### Basic Queries 226 | 227 | Query all tables in the database: 228 | 229 | ```html 230 | {% query "SELECT name, type FROM sqlite_master WHERE type='table'" %} 231 | ``` 232 | 233 | Query specific data with conditions: 234 | 235 | ```html 236 | {% query "SELECT id, name, email FROM users WHERE status = 'active'" %} 237 | ``` 238 | 239 | #### Advanced Queries 240 | 241 | Perform joins across tables: 242 | 243 | ```html 244 | {% query "SELECT o.id, u.name, o.total FROM orders o JOIN users u ON o.user_id = u.id" %} 245 | ``` 246 | 247 | Aggregate functions: 248 | 249 | ```html 250 | {% query "SELECT COUNT(*) AS total, AVG(price) AS average FROM products" %} 251 | ``` 252 | 253 | Grouping and ordering: 254 | 255 | ```html 256 | {% query "SELECT category, COUNT(*) AS count FROM products GROUP BY category ORDER BY count DESC" %} 257 | ``` 258 | 259 | ### Form-Based Database Operations 260 | 261 | Blink supports form-based database operations through POST requests to the `/sql` endpoint. 262 | 263 | #### Creating Tables 264 | 265 | ```html 266 |
267 | 268 | 274 | 275 |
276 | ``` 277 | 278 | #### Inserting Data 279 | 280 | ```html 281 |
282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |
294 | ``` 295 | 296 | #### Updating Data 297 | 298 | ```html 299 |
300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 |
312 | ``` 313 | 314 | #### Deleting Data 315 | 316 | ```html 317 |
318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 |
327 | ``` 328 | 329 | ### Placeholder Substitution 330 | 331 | In SQL forms, you can use placeholders surrounded by square brackets `[placeholder]` to be replaced with form field values: 332 | 333 | ```html 334 | INSERT INTO users (name, email, age) VALUES ('[name]', '[email]', [age]) 335 | ``` 336 | 337 | Note that for numeric values (like `[age]`), the brackets don't include quotes, allowing the value to be treated as a number. 338 | 339 | ### Error Handling 340 | 341 | If a query fails (e.g., syntax error, nonexistent table), an error message will be displayed. For successful queries: 342 | 343 | - SELECT queries will show results in a formatted table 344 | - INSERT, UPDATE, DELETE will show success message with affected row count 345 | - CREATE, DROP, etc. will show success message 346 | 347 | ## Nested Elements 348 | 349 | The template engine supports nesting conditional blocks and loops. 350 | 351 | ### Nested Conditionals 352 | 353 | ```html 354 | {% if is_logged_in %} 355 |
356 | {% if is_admin %} 357 | Admin Panel 358 | {% else %} 359 |

Welcome, standard user!

360 | {% endif %} 361 |
362 | {% endif %} 363 | ``` 364 | 365 | ### Nested Loops 366 | 367 | ```html 368 | {% for category in categories %} 369 |

{{category.0}}

370 | 375 | {% endfor %} 376 | ``` 377 | 378 | ## Best Practices 379 | 380 | ### Performance Considerations 381 | 382 | - Keep SQL queries simple and optimized 383 | - Avoid excessive nesting of conditionals and loops 384 | - Use specific column names in SELECT queries rather than * 385 | - Add LIMIT clauses to queries when appropriate 386 | 387 | ### Template Organization 388 | 389 | - Use clear, descriptive variable names 390 | - Comment your templates for clarity 391 | - Break complex templates into smaller, reusable parts 392 | - Organize your data efficiently with pipe-delimited values 393 | 394 | ### Security 395 | 396 | - Never include sensitive information in template comments 397 | - Avoid using direct user input in SQL queries 398 | - Validate all form input on the server 399 | - Use appropriate data types for SQL fields (e.g., INTEGER for numbers) 400 | 401 | ### Debugging 402 | 403 | - Check server logs for SQL errors 404 | - Verify database connection when template features aren't working 405 | - Test templates with sample data before using in production 406 | - Validate HTML output for proper structure 407 | 408 | ## Full Example 409 | 410 | Here's a complete example combining multiple template features: 411 | 412 | ```html 413 | 414 | 415 | 416 | {{page_title}} 417 | 422 | 423 | 424 | 425 | 431 | 432 |

{{page_title}}

433 | 434 | {% if is_admin %} 435 |
436 |

Admin Controls

437 |

Welcome, Administrator!

438 |
439 | {% endif %} 440 | 441 |

Product Categories

442 | 450 | 451 |

High-Stock Categories

452 | 457 | 458 |

Recent Products

459 | {% query "SELECT id, name, price, category FROM products ORDER BY id DESC LIMIT 5" %} 460 | 461 |

Add New Product

462 |
463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 477 | 478 | 479 | 480 | 481 |
482 | 483 | 484 | ``` -------------------------------------------------------------------------------- /include/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef DEBUG_H 2 | #define DEBUG_H 3 | 4 | // Uncomment the line below to enable debug messages 5 | // #define DEBUG_MODE 6 | 7 | #endif /* DEBUG_H */ -------------------------------------------------------------------------------- /include/file_watcher.h: -------------------------------------------------------------------------------- 1 | #ifndef FILE_WATCHER_H 2 | #define FILE_WATCHER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define EVENT_SIZE (sizeof(struct inotify_event)) 10 | #define BUF_LEN (1024 * (EVENT_SIZE + 16)) 11 | 12 | typedef struct { 13 | char* path; 14 | time_t last_modified; 15 | } file_info_t; 16 | 17 | typedef struct { 18 | char* directory; 19 | bool* file_changed; 20 | pthread_mutex_t* mutex; 21 | } watcher_args_t; 22 | 23 | int init_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex); 24 | pthread_t start_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex); 25 | bool file_has_changed(const char* filename, time_t* last_modified); 26 | void* watch_files(void* args); 27 | 28 | #endif -------------------------------------------------------------------------------- /include/html_serve.h: -------------------------------------------------------------------------------- 1 | #ifndef HTML_SERVE_H 2 | #define HTML_SERVE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | char* serve_html(const char* filename); 9 | char* inject_hot_reload_js(char* html_content); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /include/request_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef REQUEST_HANDLER_H 2 | #define REQUEST_HANDLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "html_serve.h" 9 | #include "template.h" 10 | #include "websocket.h" 11 | #include "server.h" 12 | #include "sqlite_handler.h" 13 | 14 | #define BUFFER_SIZE 1024 15 | 16 | extern ws_clients_t* ws_clients; 17 | extern bool enable_templates; 18 | 19 | void handle_client(int new_socket); 20 | void handle_websocket_client(int new_socket, ws_clients_t* clients); 21 | int is_websocket_request(const char* buffer); 22 | bool has_template_features(const char* content); 23 | void set_template_settings(bool enabled); 24 | void set_custom_html_file(const char* file_path); 25 | void set_server_port(int port); 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /include/server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "socket_utils.h" 12 | #include "request_handler.h" 13 | #include "file_watcher.h" 14 | #include "websocket.h" 15 | 16 | #define PORT 8080 17 | #define BUFFER_SIZE 1024 18 | #define HTML_DIR "../www" 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /include/socket_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef SOCKET_UTILS_H 2 | #define SOCKET_UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define PORT 8080 13 | #define BUFFER_SIZE 1024 14 | 15 | int initialize_server(struct sockaddr_in* address); 16 | void read_client_data(int socket, char* buffer); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /include/sqlite_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef SQLITE_HANDLER_H 2 | #define SQLITE_HANDLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | typedef struct { 11 | char*** rows; 12 | char** columns; 13 | int row_count; 14 | int column_count; 15 | int capacity; 16 | } sqlite_result_t; 17 | 18 | int init_sqlite(const char* db_path); 19 | void close_sqlite(); 20 | sqlite_result_t* execute_query(const char* query); 21 | void free_query_results(sqlite_result_t* results); 22 | bool is_db_initialized(); 23 | const char* get_db_path(); 24 | void set_db_path(const char* path); 25 | char* process_sqlite_queries(char* content); 26 | char* generate_table_html(sqlite_result_t* result); 27 | 28 | #endif -------------------------------------------------------------------------------- /include/template.h: -------------------------------------------------------------------------------- 1 | #ifndef TEMPLATE_H 2 | #define TEMPLATE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | typedef struct { 11 | char** keys; 12 | char** values; 13 | int count; 14 | int capacity; 15 | } template_data_t; 16 | 17 | char* replace_placeholders(char* result, const char** keys, const char** values, int num_pairs); 18 | char* process_if_else(char* result, const char** keys, const char** values, int num_pairs); 19 | char* process_loops(char* result, const char* loop_key, const char** loop_values, int loop_count); 20 | char* process_template(const char* template, const char** keys, const char** values, int num_pairs, const char* loop_key, const char** loop_values, int loop_count); 21 | char* process_template_auto(const char* template, const char** prog_keys, const char** prog_values, int prog_pairs); 22 | char* get_item_part(const char* item, char delimiter, int part_index); 23 | 24 | 25 | template_data_t* init_template_data(void); 26 | void add_template_var(template_data_t* data, const char* key, const char* value); 27 | template_data_t* parse_template_variables(const char* template_str); 28 | void free_template_data(template_data_t* data); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /include/websocket.h: -------------------------------------------------------------------------------- 1 | #ifndef WEBSOCKET_H 2 | #define WEBSOCKET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define MAX_CLIENTS 50 13 | #define BUFFER_SIZE 1024 14 | #define WS_HANDSHAKE_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 15 | #define WS_RESPONSE "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n" 16 | 17 | #define WS_TEXT 0x01 18 | #define WS_BINARY 0x02 19 | #define WS_CLOSE 0x08 20 | #define WS_PING 0x09 21 | #define WS_PONG 0x0A 22 | 23 | #define COLOR_RESET "\x1B[0m" 24 | #define COLOR_RED "\x1B[31m" 25 | #define COLOR_GREEN "\x1B[32m" 26 | #define COLOR_YELLOW "\x1B[33m" 27 | #define COLOR_BLUE "\x1B[34m" 28 | #define COLOR_MAGENTA "\x1B[35m" 29 | #define COLOR_CYAN "\x1B[36m" 30 | #define COLOR_WHITE "\x1B[37m" 31 | #define BOLD "\x1B[1m" 32 | #define UNDERLINE "\x1B[4m" 33 | 34 | typedef struct { 35 | int client_sockets[MAX_CLIENTS]; 36 | int count; 37 | pthread_mutex_t mutex; 38 | } ws_clients_t; 39 | 40 | ws_clients_t* init_ws_clients(); 41 | void add_ws_client(ws_clients_t* clients, int socket_fd); 42 | void remove_ws_client(ws_clients_t* clients, int socket_fd); 43 | bool is_client_connected(int socket_fd); 44 | int process_ws_handshake(int client_socket, char* buffer); 45 | int send_ws_frame(int client_socket, const char* message, size_t length, int opcode); 46 | void broadcast_to_ws_clients(ws_clients_t* clients, const char* message); 47 | void handle_ws_connection(int client_socket, ws_clients_t* clients); 48 | void free_ws_clients(ws_clients_t* clients); 49 | 50 | #endif -------------------------------------------------------------------------------- /src/file_watcher.c: -------------------------------------------------------------------------------- 1 | #include "file_watcher.h" 2 | #include "websocket.h" 3 | #include "request_handler.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | typedef struct { 15 | char** filenames; 16 | time_t* last_modified; 17 | int count; 18 | int capacity; 19 | } html_files_t; 20 | 21 | static html_files_t* html_files = NULL; 22 | 23 | static html_files_t* init_html_files() { 24 | html_files_t* files = malloc(sizeof(html_files_t)); 25 | if (!files) return NULL; 26 | 27 | files->capacity = 10; 28 | files->count = 0; 29 | files->filenames = malloc(files->capacity * sizeof(char*)); 30 | files->last_modified = malloc(files->capacity * sizeof(time_t)); 31 | 32 | if (!files->filenames || !files->last_modified) { 33 | if (files->filenames) free(files->filenames); 34 | if (files->last_modified) free(files->last_modified); 35 | free(files); 36 | return NULL; 37 | } 38 | 39 | return files; 40 | } 41 | 42 | static void add_html_file(html_files_t* files, const char* filename) { 43 | if (!files || !filename) return; 44 | for (int i = 0; i < files->count; i++) { 45 | if (strcmp(files->filenames[i], filename) == 0) { 46 | return; 47 | } 48 | } 49 | 50 | if (files->count >= files->capacity) { 51 | int new_capacity = files->capacity * 2; 52 | char** new_filenames = realloc(files->filenames, new_capacity * sizeof(char*)); 53 | time_t* new_last_modified = realloc(files->last_modified, new_capacity * sizeof(time_t)); 54 | 55 | if (!new_filenames || !new_last_modified) { 56 | if (new_filenames) files->filenames = new_filenames; 57 | fprintf(stderr, "%s%s[WARNING] %sMemory allocation failed, continuing with existing capacity%s\n", 58 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RESET); 59 | return; 60 | } 61 | 62 | files->filenames = new_filenames; 63 | files->last_modified = new_last_modified; 64 | files->capacity = new_capacity; 65 | } 66 | 67 | files->filenames[files->count] = strdup(filename); 68 | files->last_modified[files->count] = 0; 69 | files->count++; 70 | 71 | printf("%s%s[FILE WATCHER] %sAdded HTML file to watch: %s%s%s\n", 72 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, filename, COLOR_RESET); 73 | } 74 | 75 | static void scan_directory(const char* directory, html_files_t* files) { 76 | DIR* dir = opendir(directory); 77 | if (!dir) { 78 | fprintf(stderr, "%s%s[ERROR] %sFailed to open directory: %s%s\n", 79 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 80 | return; 81 | } 82 | 83 | struct dirent* entry; 84 | while ((entry = readdir(dir)) != NULL) { 85 | if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { 86 | continue; 87 | } 88 | 89 | char* extension = strrchr(entry->d_name, '.'); 90 | if (extension && strcmp(extension, ".html") == 0) { 91 | char full_path[512]; 92 | snprintf(full_path, sizeof(full_path), "%s/%s", directory, entry->d_name); 93 | add_html_file(files, full_path); 94 | } 95 | } 96 | 97 | closedir(dir); 98 | } 99 | 100 | static void add_custom_html_file_if_exists(html_files_t* files) { 101 | extern char* custom_html_file; 102 | 103 | if (custom_html_file && *custom_html_file) { 104 | struct stat st; 105 | if (stat(custom_html_file, &st) == 0 && S_ISREG(st.st_mode)) { 106 | bool already_watching = false; 107 | for (int i = 0; i < files->count; i++) { 108 | if (strcmp(files->filenames[i], custom_html_file) == 0) { 109 | already_watching = true; 110 | break; 111 | } 112 | } 113 | 114 | if (!already_watching) { 115 | printf("%s%s[FILE WATCHER] %sAdding custom HTML file to watch: %s%s%s\n", 116 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, custom_html_file, COLOR_RESET); 117 | add_html_file(files, custom_html_file); 118 | } 119 | } else { 120 | fprintf(stderr, "%s%s[WARNING] %sCustom HTML file not found or not accessible: %s%s\n", 121 | BOLD, COLOR_YELLOW, COLOR_RESET, custom_html_file, COLOR_RESET); 122 | } 123 | } 124 | } 125 | 126 | static void free_html_files(html_files_t* files) { 127 | if (!files) return; 128 | 129 | for (int i = 0; i < files->count; i++) { 130 | free(files->filenames[i]); 131 | } 132 | 133 | free(files->filenames); 134 | free(files->last_modified); 135 | free(files); 136 | } 137 | 138 | int init_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex) { 139 | if (!directory || !file_changed || !mutex) { 140 | return -1; 141 | } 142 | 143 | struct stat st; 144 | if (stat(directory, &st) == -1) { 145 | fprintf(stderr, "%s%s[ERROR] %sError checking directory: %s%s\n", 146 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 147 | return -1; 148 | } 149 | 150 | if (!S_ISDIR(st.st_mode)) { 151 | fprintf(stderr, "%s%s[ERROR] %s%s is not a directory%s\n", 152 | BOLD, COLOR_RED, COLOR_RESET, directory, COLOR_RESET); 153 | return -1; 154 | } 155 | 156 | *file_changed = false; 157 | 158 | html_files = init_html_files(); 159 | if (!html_files) { 160 | fprintf(stderr, "%s%s[ERROR] %sFailed to initialize HTML file tracking%s\n", 161 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 162 | return -1; 163 | } 164 | 165 | scan_directory(directory, html_files); 166 | 167 | add_custom_html_file_if_exists(html_files); 168 | 169 | printf("%s%s[FILE WATCHER] %sInitialized for directory: %s%s%s (tracking %s%d%s HTML files)\n", 170 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_CYAN, directory, COLOR_RESET, 171 | COLOR_YELLOW, html_files->count, COLOR_RESET); 172 | 173 | return 0; 174 | } 175 | 176 | pthread_t start_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex) { 177 | pthread_t thread_id; 178 | watcher_args_t* args = malloc(sizeof(watcher_args_t)); 179 | 180 | if (!args) { 181 | fprintf(stderr, "%s%s[ERROR] %sFailed to allocate memory for watcher args: %s%s\n", 182 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 183 | return 0; 184 | } 185 | 186 | args->directory = strdup(directory); 187 | args->file_changed = file_changed; 188 | args->mutex = mutex; 189 | 190 | if (pthread_create(&thread_id, NULL, watch_files, args) != 0) { 191 | fprintf(stderr, "%s%s[ERROR] %sFailed to create file watcher thread: %s%s\n", 192 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 193 | free(args->directory); 194 | free(args); 195 | return 0; 196 | } 197 | 198 | return thread_id; 199 | } 200 | 201 | bool file_has_changed(const char* filename, time_t* last_modified) { 202 | struct stat attr; 203 | if (stat(filename, &attr) == 0) { 204 | if (*last_modified == 0 || attr.st_mtime > *last_modified) { 205 | time_t old_time = *last_modified; 206 | *last_modified = attr.st_mtime; 207 | return (old_time != 0 && old_time != attr.st_mtime); 208 | } 209 | } 210 | return false; 211 | } 212 | 213 | static bool check_all_files_for_changes(html_files_t* files) { 214 | if (!files || files->count == 0) return false; 215 | 216 | static char* last_changed_file = NULL; 217 | static time_t last_change_time = 0; 218 | time_t current_time = time(NULL); 219 | const int SAME_FILE_DEBOUNCE_SECS = 5; 220 | 221 | bool any_changed = false; 222 | for (int i = 0; i < files->count; i++) { 223 | if (file_has_changed(files->filenames[i], &files->last_modified[i])) { 224 | bool is_repeat = (last_changed_file && strcmp(files->filenames[i], last_changed_file) == 0); 225 | bool within_debounce = (difftime(current_time, last_change_time) < SAME_FILE_DEBOUNCE_SECS); 226 | 227 | if (!is_repeat || !within_debounce) { 228 | printf("%s%s[FILE WATCHER] %sDetected change in file: %s%s%s\n", 229 | BOLD, COLOR_BLUE, COLOR_YELLOW, COLOR_CYAN, 230 | files->filenames[i], COLOR_RESET); 231 | 232 | if (last_changed_file) { 233 | free(last_changed_file); 234 | } 235 | last_changed_file = strdup(files->filenames[i]); 236 | last_change_time = current_time; 237 | } 238 | 239 | any_changed = true; 240 | } 241 | } 242 | 243 | return any_changed; 244 | } 245 | 246 | static void rescan_directory_if_needed(const char* directory, html_files_t* files) { 247 | static time_t last_scan_time = 0; 248 | time_t current_time = time(NULL); 249 | 250 | if (difftime(current_time, last_scan_time) > 30) { 251 | printf("%s%s[FILE WATCHER] %sRescanning directory for new HTML files...%s\n", 252 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 253 | 254 | scan_directory(directory, files); 255 | add_custom_html_file_if_exists(files); 256 | 257 | last_scan_time = current_time; 258 | } 259 | } 260 | 261 | void* watch_files(void* args) { 262 | watcher_args_t* watcher_args = (watcher_args_t*)args; 263 | int fd, wd, custom_file_wd = -1; 264 | char buffer[BUF_LEN]; 265 | extern char* custom_html_file; 266 | 267 | time_t last_notification_time = 0; 268 | const int DEBOUNCE_TIME_MS = 1000; 269 | fd = inotify_init(); 270 | if (fd < 0) { 271 | fprintf(stderr, "%s%s[ERROR] %sinotify_init failed: %s%s\n", 272 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 273 | free(watcher_args->directory); 274 | free(watcher_args); 275 | return NULL; 276 | } 277 | 278 | wd = inotify_add_watch(fd, watcher_args->directory, 279 | IN_MODIFY | IN_CREATE | IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB); 280 | if (wd < 0) { 281 | fprintf(stderr, "%s%s[ERROR] %sinotify_add_watch failed: %s%s\n", 282 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 283 | close(fd); 284 | free(watcher_args->directory); 285 | free(watcher_args); 286 | return NULL; 287 | } 288 | 289 | if (custom_html_file && *custom_html_file) { 290 | char* last_slash = strrchr(custom_html_file, '/'); 291 | if (last_slash) { 292 | char custom_dir[512] = {0}; 293 | strncpy(custom_dir, custom_html_file, last_slash - custom_html_file); 294 | custom_dir[last_slash - custom_html_file] = '\0'; 295 | 296 | if (strcmp(custom_dir, watcher_args->directory) != 0) { 297 | custom_file_wd = inotify_add_watch(fd, custom_dir, 298 | IN_MODIFY | IN_CREATE | IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB); 299 | 300 | if (custom_file_wd >= 0) { 301 | printf("%s%s[FILE WATCHER] %sAdded watch for custom HTML file directory: %s%s%s\n", 302 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, custom_dir, COLOR_RESET); 303 | } 304 | } 305 | } 306 | } 307 | 308 | printf("%s%s[FILE WATCHER] %sActive - watching directory: %s%s%s\n", 309 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, watcher_args->directory, COLOR_RESET); 310 | 311 | while (1) { 312 | bool change_detected = false; 313 | if (check_all_files_for_changes(html_files)) { 314 | change_detected = true; 315 | } 316 | 317 | fd_set read_fds; 318 | FD_ZERO(&read_fds); 319 | FD_SET(fd, &read_fds); 320 | struct timeval tv; 321 | tv.tv_sec = 0; 322 | tv.tv_usec = 100000; 323 | 324 | int ret = select(fd + 1, &read_fds, NULL, NULL, &tv); 325 | 326 | if (ret > 0 && FD_ISSET(fd, &read_fds)) { 327 | int length = read(fd, buffer, BUF_LEN); 328 | if (length > 0) { 329 | int i = 0; 330 | while (i < length) { 331 | struct inotify_event* event = (struct inotify_event*)&buffer[i]; 332 | 333 | if (event->len > 0) { 334 | char* dot = strrchr(event->name, '.'); 335 | if (dot && (strcmp(dot, ".html") == 0)) { 336 | printf("%s%s[FILE WATCHER] %sEvent detected: %s%s%s (mask: 0x%08x)\n", 337 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_CYAN, event->name, COLOR_RESET, event->mask); 338 | change_detected = true; 339 | char full_path[512]; 340 | snprintf(full_path, sizeof(full_path), "%s/%s", 341 | watcher_args->directory, event->name); 342 | add_html_file(html_files, full_path); 343 | } 344 | } 345 | 346 | i += EVENT_SIZE + event->len; 347 | } 348 | } 349 | } else if (ret < 0 && errno != EINTR) { 350 | fprintf(stderr, "%s%s[FILE WATCHER] %sSelect error: %s%s\n", 351 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 352 | } 353 | 354 | rescan_directory_if_needed(watcher_args->directory, html_files); 355 | if (change_detected) { 356 | time_t current_time = time(NULL); 357 | double ms_since_last = difftime(current_time, last_notification_time) * 1000; 358 | if (ms_since_last >= DEBOUNCE_TIME_MS) { 359 | bool already_signaled = false; 360 | pthread_mutex_lock(watcher_args->mutex); 361 | already_signaled = *(watcher_args->file_changed); 362 | pthread_mutex_unlock(watcher_args->mutex); 363 | 364 | if (!already_signaled) { 365 | usleep(100000); 366 | pthread_mutex_lock(watcher_args->mutex); 367 | *(watcher_args->file_changed) = true; 368 | pthread_mutex_unlock(watcher_args->mutex); 369 | printf("%s%s[FILE WATCHER] %sSignaled main thread about file changes%s\n", 370 | BOLD, COLOR_BLUE, COLOR_YELLOW, COLOR_RESET); 371 | last_notification_time = current_time; 372 | } else { 373 | static time_t last_skip_message = 0; 374 | if (difftime(current_time, last_skip_message) > 5) { 375 | printf("%s%s[FILE WATCHER] %sSkipping notification - one already pending%s\n", 376 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 377 | last_skip_message = current_time; 378 | } 379 | } 380 | } else { 381 | static time_t last_debounce_message = 0; 382 | if (difftime(current_time, last_debounce_message) > 5) { 383 | printf("%s%s[FILE WATCHER] %sDebouncing - %d ms since last notification%s\n", 384 | BOLD, COLOR_BLUE, COLOR_CYAN, (int)ms_since_last, COLOR_RESET); 385 | last_debounce_message = current_time; 386 | } 387 | } 388 | } 389 | usleep(100000); 390 | } 391 | 392 | if (custom_file_wd >= 0) { 393 | inotify_rm_watch(fd, custom_file_wd); 394 | } 395 | inotify_rm_watch(fd, wd); 396 | close(fd); 397 | 398 | free(watcher_args->directory); 399 | free(watcher_args); 400 | free_html_files(html_files); 401 | 402 | return NULL; 403 | } -------------------------------------------------------------------------------- /src/handle_client.c: -------------------------------------------------------------------------------- 1 | #include "handle_client.h" 2 | #include "websocket.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define BUFFER_SIZE 1024 13 | #define MAX_PATH_LEN 256 14 | #define READ_TIMEOUT_SECS 5 15 | 16 | static int set_socket_timeout(int sockfd, int seconds) { 17 | struct timeval tv; 18 | tv.tv_sec = seconds; 19 | tv.tv_usec = 0; 20 | if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { 21 | perror("setsockopt - SO_RCVTIMEO"); 22 | return -1; 23 | } 24 | if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) { 25 | perror("setsockopt - SO_SNDTIMEO"); 26 | return -1; 27 | } 28 | 29 | return 0; 30 | } 31 | 32 | const char* get_content_type(const char* path) { 33 | const char* ext = strrchr(path, '.'); 34 | if (ext) { 35 | if (strcmp(ext, ".html") == 0) return "text/html"; 36 | if (strcmp(ext, ".css") == 0) return "text/css"; 37 | if (strcmp(ext, ".js") == 0) return "application/javascript"; 38 | if (strcmp(ext, ".json") == 0) return "application/json"; 39 | if (strcmp(ext, ".png") == 0) return "image/png"; 40 | if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) return "image/jpeg"; 41 | if (strcmp(ext, ".gif") == 0) return "image/gif"; 42 | if (strcmp(ext, ".svg") == 0) return "image/svg+xml"; 43 | if (strcmp(ext, ".ico") == 0) return "image/x-icon"; 44 | } 45 | return "text/plain"; 46 | } 47 | 48 | static int read_request_line(int client_socket, char* buffer, size_t buffer_size) { 49 | if (set_socket_timeout(client_socket, READ_TIMEOUT_SECS) < 0) { 50 | return -1; 51 | } 52 | size_t total_read = 0; 53 | char c; 54 | ssize_t bytes_read; 55 | 56 | while (total_read < buffer_size - 1) { 57 | bytes_read = recv(client_socket, &c, 1, 0); 58 | 59 | if (bytes_read <= 0) { 60 | if (bytes_read < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { 61 | fprintf(stderr, "Timeout while reading request\n"); 62 | } else if (bytes_read < 0) { 63 | perror("Error reading from socket"); 64 | } 65 | return -1; 66 | } 67 | buffer[total_read++] = c; 68 | if (total_read >= 2 && 69 | buffer[total_read-2] == '\r' && 70 | buffer[total_read-1] == '\n') { 71 | break; 72 | } 73 | } 74 | 75 | buffer[total_read] = '\0'; 76 | return total_read; 77 | } 78 | 79 | void handle_client(int client_socket) { 80 | char buffer[BUFFER_SIZE] = {0}; 81 | char path[MAX_PATH_LEN] = {0}; 82 | char full_path[MAX_PATH_LEN] = {0}; 83 | int file_fd = -1; 84 | if (read_request_line(client_socket, buffer, BUFFER_SIZE) <= 0) { 85 | close(client_socket); 86 | return; 87 | } 88 | if (sscanf(buffer, "GET %s", path) != 1) { 89 | fprintf(stderr, "Invalid request format: %s\n", buffer); 90 | close(client_socket); 91 | return; 92 | } 93 | 94 | while (read_request_line(client_socket, buffer, BUFFER_SIZE) > 0) { 95 | if (strcmp(buffer, "\r\n") == 0) { 96 | break; 97 | } 98 | } 99 | if (strcmp(path, "/") == 0) { 100 | strcpy(path, "/index.html"); 101 | } 102 | 103 | snprintf(full_path, MAX_PATH_LEN, "%s%s", HTML_DIR, path); 104 | struct stat file_stat; 105 | if (stat(full_path, &file_stat) == 0 && S_ISREG(file_stat.st_mode)) { 106 | file_fd = open(full_path, O_RDONLY); 107 | } 108 | 109 | if (file_fd == -1) { 110 | const char* not_found = "HTTP/1.1 404 Not Found\r\nContent-Length: 26\r\nContent-Type: text/html\r\n\r\n

404 - Not Found

"; 111 | send(client_socket, not_found, strlen(not_found), 0); 112 | } else { 113 | const char* content_type = get_content_type(path); 114 | char headers[BUFFER_SIZE]; 115 | snprintf(headers, BUFFER_SIZE, 116 | "HTTP/1.1 200 OK\r\n" 117 | "Content-Length: %ld\r\n" 118 | "Content-Type: %s\r\n" 119 | "Connection: close\r\n" 120 | "\r\n", 121 | file_stat.st_size, content_type); 122 | 123 | if (send(client_socket, headers, strlen(headers), 0) < 0) { 124 | perror("Error sending headers"); 125 | close(file_fd); 126 | close(client_socket); 127 | return; 128 | } 129 | ssize_t bytes_read; 130 | while ((bytes_read = read(file_fd, buffer, BUFFER_SIZE)) > 0) { 131 | ssize_t bytes_sent = send(client_socket, buffer, bytes_read, 0); 132 | if (bytes_sent < 0) { 133 | if (errno == EPIPE || errno == ECONNRESET) { 134 | break; 135 | } 136 | perror("Error sending file content"); 137 | break; 138 | } 139 | } 140 | close(file_fd); 141 | } 142 | close(client_socket); 143 | } 144 | 145 | int is_websocket_request(const char* request) { 146 | return (strstr(request, "Upgrade: websocket") != NULL) ? 1 : 0; 147 | } 148 | 149 | void handle_websocket_client(int client_socket, ws_clients_t* clients) { 150 | char buffer[BUFFER_SIZE] = {0}; 151 | ssize_t bytes_read = recv(client_socket, buffer, BUFFER_SIZE - 1, 0); 152 | if (bytes_read <= 0) { 153 | close(client_socket); 154 | return; 155 | } 156 | buffer[bytes_read] = '\0'; 157 | 158 | if (process_ws_handshake(client_socket, buffer)) { 159 | add_ws_client(clients, client_socket); 160 | printf("%s%s[WebSocket] %s%sClient connected%s\n", 161 | BOLD, COLOR_BLUE, BOLD, COLOR_GREEN, COLOR_RESET); 162 | } else { 163 | fprintf(stderr, "%s%s[WebSocket] %s%sFailed to process WebSocket handshake%s\n", 164 | BOLD, COLOR_RED, BOLD, COLOR_YELLOW, COLOR_RESET); 165 | close(client_socket); 166 | } 167 | } 168 | 169 | int process_ws_handshake(int client_socket, char* buffer) { 170 | return process_ws_handshake(client_socket, buffer); 171 | } -------------------------------------------------------------------------------- /src/html_serve.c: -------------------------------------------------------------------------------- 1 | #include "html_serve.h" 2 | 3 | char* serve_html(const char* filename) { 4 | FILE* file = fopen(filename, "r"); 5 | if (!file) { 6 | perror("Error opening file"); 7 | return NULL; 8 | } 9 | if (fseek(file, 0, SEEK_END) != 0) { 10 | perror("Error seeking to end of file"); 11 | fclose(file); 12 | return NULL; 13 | } 14 | 15 | long length = ftell(file); 16 | if (length == -1) { 17 | perror("Error getting file size"); 18 | fclose(file); 19 | return NULL; 20 | } 21 | 22 | if (fseek(file, 0, SEEK_SET) != 0) { 23 | perror("Error seeking to beginning of file"); 24 | fclose(file); 25 | return NULL; 26 | } 27 | 28 | char* buffer = malloc(length + 1); 29 | if (!buffer) { 30 | perror("Error allocating memory"); 31 | fclose(file); 32 | return NULL; 33 | } 34 | 35 | if (fread(buffer, 1, length, file) != length) { 36 | perror("Error reading file"); 37 | free(buffer); 38 | fclose(file); 39 | return NULL; 40 | } 41 | 42 | buffer[length] = '\0'; 43 | fclose(file); 44 | return buffer; 45 | } 46 | 47 | char* inject_hot_reload_js(char* html_content) { 48 | if (!html_content) { 49 | fprintf(stderr, "Error: Null HTML content passed to inject_hot_reload_js\n"); 50 | return NULL; 51 | } 52 | 53 | const char* hot_reload_js = 54 | "\n"; 108 | 109 | char* body_close_tag = strstr(html_content, ""); 110 | if (!body_close_tag) { 111 | printf("No tag found in HTML content, not injecting hot reload script\n"); 112 | return html_content; 113 | } 114 | 115 | size_t original_len = strlen(html_content); 116 | size_t script_len = strlen(hot_reload_js); 117 | size_t offset = body_close_tag - html_content; 118 | 119 | char* new_html = malloc(original_len + script_len + 1); 120 | if (!new_html) { 121 | perror("Memory allocation failed for hot reload script injection"); 122 | return html_content; 123 | } 124 | 125 | strncpy(new_html, html_content, offset); 126 | new_html[offset] = '\0'; 127 | strcat(new_html, hot_reload_js); 128 | strcat(new_html, body_close_tag); 129 | 130 | free(html_content); 131 | return new_html; 132 | } 133 | -------------------------------------------------------------------------------- /src/server.c: -------------------------------------------------------------------------------- 1 | #include "server.h" 2 | #include 3 | #include 4 | #include 5 | #include "sqlite_handler.h" 6 | #include "debug.h" 7 | 8 | #define PORT 8080 9 | #define BUFFER_SIZE 1024 10 | #define RELOAD_DELAY_MS 300 11 | #define SHUTDOWN_TIMEOUT_SEC 5 12 | 13 | typedef struct { 14 | ws_clients_t* clients; 15 | bool* file_changed; 16 | pthread_mutex_t* mutex; 17 | } ws_monitor_args_t; 18 | 19 | volatile sig_atomic_t server_running = 1; 20 | volatile sig_atomic_t shutdown_in_progress = 0; 21 | pthread_t watcher_thread = 0; 22 | pthread_t monitor_thread = 0; 23 | ws_clients_t* ws_clients = NULL; 24 | pthread_mutex_t* file_mutex_ptr = NULL; 25 | ws_monitor_args_t* monitor_args = NULL; 26 | 27 | void cleanup_resources(void); 28 | 29 | void signal_handler(int signum) { 30 | if (shutdown_in_progress) { 31 | printf("\n%s%s[SERVER] %s%s[SERVER] Forced shutdown - terminating immediately%s\n", 32 | BOLD, COLOR_RED, BOLD, COLOR_YELLOW, COLOR_RESET); 33 | 34 | _exit(EXIT_FAILURE); 35 | } 36 | 37 | shutdown_in_progress = 1; 38 | server_running = 0; 39 | 40 | printf("\n%s%s[SERVER] %sReceived signal %d. Initiating shutdown sequence...%s\n", 41 | BOLD, COLOR_BLUE, COLOR_YELLOW, signum, COLOR_RESET); 42 | printf("%s%s[SERVER] %sPlease wait for cleanup to complete (press Ctrl+C again for forced exit)%s\n", 43 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 44 | 45 | struct sigaction forced_exit; 46 | memset(&forced_exit, 0, sizeof(forced_exit)); 47 | forced_exit.sa_handler = signal_handler; 48 | sigaction(SIGINT, &forced_exit, NULL); 49 | sigaction(SIGTERM, &forced_exit, NULL); 50 | } 51 | 52 | void* shutdown_watchdog(void* arg) { 53 | int timeout = SHUTDOWN_TIMEOUT_SEC; 54 | printf("%s%s[SERVER] %sShutdown watchdog started (timeout: %d seconds)%s\n", 55 | BOLD, COLOR_BLUE, COLOR_CYAN, timeout, COLOR_RESET); 56 | 57 | sleep(timeout); 58 | if (shutdown_in_progress) { 59 | printf("%s%s[SERVER] %s%sShutdown timeout exceeded! Forcing exit...%s\n", 60 | BOLD, COLOR_RED, BOLD, COLOR_YELLOW, COLOR_RESET); 61 | _exit(EXIT_FAILURE); 62 | } 63 | 64 | return NULL; 65 | } 66 | 67 | void* ws_monitor_thread(void* args) { 68 | ws_monitor_args_t* monitor_args = (ws_monitor_args_t*)args; 69 | bool last_change_state = false; 70 | time_t last_reload_time = 0; 71 | const int RELOAD_COOLDOWN_SEC = 1; 72 | const int PING_INTERVAL_MS = 10000; 73 | time_t last_ping_time = 0; 74 | 75 | printf("%s%s[WS MONITOR] %sThread started%s\n", 76 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_RESET); 77 | 78 | while (server_running) { 79 | time_t current_time = time(NULL); 80 | bool should_notify = false; 81 | if (difftime(current_time, last_ping_time) * 1000 >= PING_INTERVAL_MS) { 82 | if (monitor_args->clients && monitor_args->clients->count > 0) { 83 | broadcast_to_ws_clients(monitor_args->clients, "ping"); 84 | last_ping_time = current_time; 85 | } 86 | } 87 | 88 | pthread_mutex_lock(monitor_args->mutex); 89 | bool current_change_state = *(monitor_args->file_changed); 90 | 91 | if (current_change_state) { 92 | if (difftime(current_time, last_reload_time) >= RELOAD_COOLDOWN_SEC) { 93 | printf("%s%s[HOT RELOAD] %sFile changes detected, preparing notification%s\n", 94 | BOLD, COLOR_MAGENTA, COLOR_RESET, COLOR_RESET); 95 | 96 | if (monitor_args->clients && monitor_args->clients->count > 0) { 97 | should_notify = true; 98 | last_reload_time = current_time; 99 | } else { 100 | printf("%s%s[HOT RELOAD] %sNo clients connected, skipping notification%s\n", 101 | BOLD, COLOR_MAGENTA, COLOR_YELLOW, COLOR_RESET); 102 | } 103 | 104 | *(monitor_args->file_changed) = false; 105 | } else { 106 | printf("%s%s[HOT RELOAD] %sChanges detected during cooldown period (%ds), deferring%s\n", 107 | BOLD, COLOR_MAGENTA, COLOR_YELLOW, RELOAD_COOLDOWN_SEC, COLOR_RESET); 108 | } 109 | } 110 | 111 | pthread_mutex_unlock(monitor_args->mutex); 112 | 113 | if (should_notify) { 114 | usleep(RELOAD_DELAY_MS * 1000); 115 | printf("%s%s[HOT RELOAD] %sNotifying %s%d%s client(s) to reload%s\n", 116 | BOLD, COLOR_MAGENTA, COLOR_RESET, 117 | COLOR_YELLOW, monitor_args->clients->count, COLOR_RESET, COLOR_RESET); 118 | 119 | broadcast_to_ws_clients(monitor_args->clients, "reload"); 120 | 121 | printf("%s%s[HOT RELOAD] %s%sReload notification sent successfully%s\n", 122 | BOLD, COLOR_MAGENTA, BOLD, COLOR_GREEN, COLOR_RESET); 123 | } 124 | 125 | last_change_state = current_change_state; 126 | usleep(100000); 127 | } 128 | 129 | printf("%s%s[WS MONITOR] %s%sThread stopped%s\n", 130 | BOLD, COLOR_BLUE, BOLD, COLOR_YELLOW, COLOR_RESET); 131 | return NULL; 132 | } 133 | 134 | void cleanup_resources(void) { 135 | static int cleanup_running = 0; 136 | if (cleanup_running) { 137 | return; 138 | } 139 | 140 | cleanup_running = 1; 141 | printf("%s%s[SERVER] %sShutdown in progress, cleaning up resources...%s\n", 142 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_RESET); 143 | pthread_t watchdog_thread; 144 | if (!pthread_create(&watchdog_thread, NULL, shutdown_watchdog, NULL)) { 145 | pthread_detach(watchdog_thread); 146 | } 147 | 148 | server_running = 0; 149 | 150 | if (is_db_initialized()) { 151 | printf("%s%s[SERVER] %sClosing SQLite database connection...%s\n", 152 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 153 | close_sqlite(); 154 | } 155 | 156 | if (ws_clients) { 157 | printf("%s%s[SERVER] %sClosing WebSocket connections...%s\n", 158 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 159 | free_ws_clients(ws_clients); 160 | ws_clients = NULL; 161 | } 162 | 163 | if (watcher_thread != 0) { 164 | printf("%s%s[SERVER] %sStopping file watcher thread...%s\n", 165 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 166 | int cancel_result = pthread_cancel(watcher_thread); 167 | if (cancel_result != 0) { 168 | fprintf(stderr, "%s%s[ERROR] %sFailed to cancel file watcher thread (error %d)%s\n", 169 | BOLD, COLOR_RED, COLOR_RESET, cancel_result, COLOR_RESET); 170 | } 171 | int join_result = pthread_join(watcher_thread, NULL); 172 | if (join_result != 0) { 173 | fprintf(stderr, "%s%s[ERROR] %sFailed to join file watcher thread (error %d)%s\n", 174 | BOLD, COLOR_RED, COLOR_RESET, join_result, COLOR_RESET); 175 | pthread_detach(watcher_thread); 176 | } else { 177 | printf("%s%s[SERVER] %sFile watcher thread stopped%s\n", 178 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_RESET); 179 | } 180 | watcher_thread = 0; 181 | } 182 | 183 | if (monitor_thread != 0) { 184 | printf("%s%s[SERVER] %sStopping WebSocket monitor thread...%s\n", 185 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET); 186 | int cancel_result = pthread_cancel(monitor_thread); 187 | if (cancel_result != 0) { 188 | fprintf(stderr, "%s%s[ERROR] %sFailed to cancel WebSocket monitor thread (error %d)%s\n", 189 | BOLD, COLOR_RED, COLOR_RESET, cancel_result, COLOR_RESET); 190 | } 191 | int join_result = pthread_join(monitor_thread, NULL); 192 | if (join_result != 0) { 193 | fprintf(stderr, "%s%s[ERROR] %sFailed to join WebSocket monitor thread (error %d)%s\n", 194 | BOLD, COLOR_RED, COLOR_RESET, join_result, COLOR_RESET); 195 | pthread_detach(monitor_thread); 196 | } else { 197 | printf("%s%s[SERVER] %sWebSocket monitor thread stopped%s\n", 198 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_RESET); 199 | } 200 | monitor_thread = 0; 201 | } 202 | 203 | if (monitor_args) { 204 | free(monitor_args); 205 | monitor_args = NULL; 206 | } 207 | 208 | if (file_mutex_ptr) { 209 | pthread_mutex_destroy(file_mutex_ptr); 210 | file_mutex_ptr = NULL; 211 | } 212 | 213 | printf("%s%s[SERVER] %s%sCleanup complete%s\n", 214 | BOLD, COLOR_BLUE, BOLD, COLOR_GREEN, COLOR_RESET); 215 | cleanup_running = 0; 216 | shutdown_in_progress = 0; 217 | } 218 | 219 | int main(int argc, char *argv[]) { 220 | int server_fd = -1, new_socket; 221 | struct sockaddr_in address; 222 | int addrlen = sizeof(address); 223 | bool file_changed = false; 224 | pthread_mutex_t file_mutex = PTHREAD_MUTEX_INITIALIZER; 225 | file_mutex_ptr = &file_mutex; 226 | int port = PORT; 227 | char* custom_html_file = NULL; 228 | char* db_path = NULL; 229 | 230 | if (argc > 1 && argv[1][0] != '-') { 231 | custom_html_file = argv[1]; 232 | printf("%s%s[CONFIG] %sDetected custom HTML file as first argument: %s%s%s\n", 233 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_CYAN, custom_html_file, COLOR_RESET); 234 | set_custom_html_file(custom_html_file); 235 | printf("%s%s[CONFIG] %sCustom HTML file provided as first argument, SQLite will be %s%s%s unless -db is specified\n", 236 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RED, "DISABLED", COLOR_RESET); 237 | 238 | close_sqlite(); 239 | set_db_path(NULL); 240 | } 241 | 242 | for (int i = 1; i < argc; i++) { 243 | if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { 244 | if (i + 1 < argc) { 245 | int custom_port = atoi(argv[i + 1]); 246 | if (custom_port > 0 && custom_port < 65536) { 247 | port = custom_port; 248 | i++; 249 | } else { 250 | fprintf(stderr, "%s%s[CONFIG] %sInvalid port number. Using default port %d%s\n", 251 | BOLD, COLOR_YELLOW, COLOR_RESET, PORT, COLOR_RESET); 252 | } 253 | } 254 | } else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--no-templates") == 0) { 255 | set_template_settings(false); 256 | printf("%s%s[CONFIG] %sTemplate processing disabled%s\n", 257 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RESET); 258 | } else if (strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--serve") == 0) { 259 | if (i + 1 < argc) { 260 | custom_html_file = argv[i + 1]; 261 | set_custom_html_file(custom_html_file); 262 | printf("%s%s[CONFIG] %sUsing custom HTML file: %s%s%s\n", 263 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_CYAN, custom_html_file, COLOR_RESET); 264 | i++; 265 | } else { 266 | fprintf(stderr, "%s%s[CONFIG] %sNo file specified after -s/--serve option%s\n", 267 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RESET); 268 | } 269 | } else if (strcmp(argv[i], "-db") == 0 || strcmp(argv[i], "--database") == 0) { 270 | if (i + 1 < argc) { 271 | db_path = argv[i + 1]; 272 | set_db_path(db_path); 273 | printf("%s%s[CONFIG] %sUsing SQLite database: %s%s%s\n", 274 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_CYAN, db_path, COLOR_RESET); 275 | i++; 276 | } else { 277 | fprintf(stderr, "%s%s[CONFIG] %sNo database path specified after -db/--database option%s\n", 278 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RESET); 279 | } 280 | } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { 281 | printf("%s%s[HELP]%s Usage: %s [OPTIONS]\n", BOLD, COLOR_BLUE, COLOR_RESET, argv[0]); 282 | printf("Options:\n"); 283 | printf(" -p, --port PORT Specify port number (default: %d)\n", PORT); 284 | printf(" -s, --serve FILE Specify a custom HTML file to serve\n"); 285 | printf(" -db, --database FILE Specify SQLite database path\n"); 286 | printf(" -n, --no-templates Disable template processing\n"); 287 | printf(" -h, --help Display this help message\n"); 288 | return EXIT_SUCCESS; 289 | } 290 | } 291 | 292 | #ifdef DEBUG_MODE 293 | printf("%s%s[DEBUG] %sCommand line arguments: custom_html_file=%s, db_path=%s%s\n", 294 | BOLD, COLOR_CYAN, COLOR_RESET, 295 | custom_html_file ? custom_html_file : "NULL", 296 | db_path ? db_path : "NULL", COLOR_RESET); 297 | #endif 298 | 299 | if (db_path != NULL) { 300 | if (init_sqlite(db_path) != 0) { 301 | fprintf(stderr, "%s%s[SQLite] %s%sFailed to initialize database%s\n", 302 | BOLD, COLOR_RED, BOLD, COLOR_RESET, COLOR_RESET); 303 | } 304 | } 305 | 306 | else if (custom_html_file == NULL) { 307 | char default_db_path[256]; 308 | snprintf(default_db_path, sizeof(default_db_path), "%s/sample.db", HTML_DIR); 309 | 310 | printf("%s%s[CONFIG] %sNo database specified, using default: %s%s%s\n", 311 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_CYAN, default_db_path, COLOR_RESET); 312 | 313 | if (init_sqlite(default_db_path) != 0) { 314 | fprintf(stderr, "%s%s[SQLite] %s%sFailed to initialize default database%s\n", 315 | BOLD, COLOR_RED, BOLD, COLOR_RESET, COLOR_RESET); 316 | } 317 | } 318 | else { 319 | // Make sure SQLite is definitely disabled 320 | printf("%s%s[CONFIG] %sCustom HTML file specified but no database path, SQLite remains %s%s%s\n", 321 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RED, "DISABLED", COLOR_RESET); 322 | 323 | close_sqlite(); 324 | set_db_path(NULL); 325 | } 326 | 327 | #ifdef DEBUG_MODE 328 | if (is_db_initialized()) { 329 | printf("%s%s[DEBUG] %sSQLite Status: %s%s%s\n", 330 | BOLD, COLOR_CYAN, COLOR_RESET, 331 | COLOR_GREEN, "ENABLED", COLOR_RESET); 332 | } else { 333 | printf("%s%s[DEBUG] %sSQLite Status: %s%s%s\n", 334 | BOLD, COLOR_CYAN, COLOR_RESET, 335 | COLOR_RED, "DISABLED", COLOR_RESET); 336 | } 337 | #endif 338 | 339 | set_server_port(port); 340 | struct sigaction sa; 341 | memset(&sa, 0, sizeof(sa)); 342 | sa.sa_handler = signal_handler; 343 | sigaction(SIGINT, &sa, NULL); 344 | sigaction(SIGTERM, &sa, NULL); 345 | signal(SIGPIPE, SIG_IGN); 346 | 347 | ws_clients = init_ws_clients(); 348 | if (!ws_clients) { 349 | fprintf(stderr, "%s%s[ERROR] %sFailed to initialize WebSocket clients%s\n", 350 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 351 | cleanup_resources(); 352 | return EXIT_FAILURE; 353 | } 354 | 355 | if (init_file_watcher(HTML_DIR, &file_changed, &file_mutex) != 0) { 356 | fprintf(stderr, "%s%s[ERROR] %sFailed to initialize file watcher%s\n", 357 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 358 | cleanup_resources(); 359 | return EXIT_FAILURE; 360 | } 361 | 362 | watcher_thread = start_file_watcher(HTML_DIR, &file_changed, &file_mutex); 363 | if (watcher_thread == 0) { 364 | fprintf(stderr, "%s%s[ERROR] %sFailed to start file watcher thread%s\n", 365 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 366 | cleanup_resources(); 367 | return EXIT_FAILURE; 368 | } 369 | 370 | monitor_args = malloc(sizeof(ws_monitor_args_t)); 371 | if (!monitor_args) { 372 | fprintf(stderr, "%s%s[ERROR] %sFailed to allocate memory for WebSocket monitor: %s%s\n", 373 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 374 | cleanup_resources(); 375 | return EXIT_FAILURE; 376 | } 377 | 378 | monitor_args->clients = ws_clients; 379 | monitor_args->file_changed = &file_changed; 380 | monitor_args->mutex = &file_mutex; 381 | if (pthread_create(&monitor_thread, NULL, ws_monitor_thread, monitor_args) != 0) { 382 | fprintf(stderr, "%s%s[ERROR] %sFailed to create WebSocket monitor thread: %s%s\n", 383 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 384 | cleanup_resources(); 385 | return EXIT_FAILURE; 386 | } 387 | 388 | 389 | memset(&address, 0, sizeof(address)); 390 | address.sin_family = AF_INET; 391 | address.sin_addr.s_addr = INADDR_ANY; 392 | address.sin_port = htons(port); 393 | server_fd = initialize_server(&address); 394 | if (server_fd == -1) { 395 | cleanup_resources(); 396 | return EXIT_FAILURE; 397 | } 398 | 399 | int opt = 1; 400 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { 401 | fprintf(stderr, "%s%s[WARNING] %sError setting SO_REUSEADDR option: %s%s\n", 402 | BOLD, COLOR_YELLOW, COLOR_RESET, strerror(errno), COLOR_RESET); 403 | } 404 | 405 | printf("\n"); 406 | printf("%s%s┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 407 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 408 | printf("%s%s┃ %s _____ _ _ _ ___ ___ ___ %s┃%s\n", BOLD, COLOR_GREEN, COLOR_WHITE, COLOR_GREEN, COLOR_RESET); 409 | printf("%s%s┃ %s| __ | |_|___| |_ |_ | | | | | %s┃%s\n", BOLD, COLOR_GREEN, COLOR_WHITE, COLOR_GREEN, COLOR_RESET); 410 | printf("%s%s┃ %s| __ -| | | | '_| _| |_ _| | |_| | | %s┃%s\n", BOLD, COLOR_GREEN, COLOR_WHITE, COLOR_GREEN, COLOR_RESET); 411 | printf("%s%s┃ %s|_____|_|_|_|_|_,_| |_____|_|___|_|___| %s┃%s\n", BOLD, COLOR_GREEN, COLOR_WHITE, COLOR_GREEN, COLOR_RESET); 412 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 413 | printf("%s%s┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 414 | printf("%s%s┃ %sS E R V E R S T A R T E D %s ┃%s\n", BOLD, COLOR_GREEN, COLOR_WHITE, COLOR_GREEN, COLOR_RESET); 415 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 416 | printf("%s%s┃ %sPORT:%s %-37d ┃%s\n", BOLD, COLOR_GREEN, COLOR_YELLOW, COLOR_CYAN, port, COLOR_RESET); 417 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 418 | printf("%s%s┃ %sHOT RELOAD:%s %-31s ┃%s\n", BOLD, COLOR_GREEN, COLOR_YELLOW, COLOR_CYAN, "ENABLED", COLOR_RESET); 419 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 420 | printf("%s%s┃ %sTEMPLATES:%s %-32s ┃%s\n", BOLD, COLOR_GREEN, COLOR_YELLOW, COLOR_CYAN, enable_templates ? "ENABLED" : "DISABLED", COLOR_RESET); 421 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 422 | printf("%s%s┃ %sSQLITE:%s %-35s ┃%s\n", BOLD, COLOR_GREEN, COLOR_YELLOW, COLOR_CYAN, is_db_initialized() ? "ENABLED" : "DISABLED", COLOR_RESET); 423 | if (is_db_initialized()) { 424 | printf("%s%s┃ %sDB PATH:%s %-33s ┃%s\n", BOLD, COLOR_GREEN, COLOR_YELLOW, COLOR_CYAN, get_db_path(), COLOR_RESET); 425 | } 426 | printf("%s%s┃ ┃%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 427 | printf("%s%s┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛%s\n", BOLD, COLOR_GREEN, COLOR_RESET); 428 | printf("\n"); 429 | printf("%s%s[SERVER] %sPress Ctrl+C to stop the server%s\n", BOLD, COLOR_BLUE, COLOR_RESET, COLOR_RESET); 430 | 431 | fd_set read_fds; 432 | struct timeval timeout; 433 | int max_fd = server_fd; 434 | time_t last_ping_time = 0; 435 | const int PING_INTERVAL_MS = 5000; 436 | 437 | while (server_running) { 438 | if (!server_running) { 439 | printf("%s%s[SERVER] %sShutdown signal received, exiting main loop...%s\n", 440 | BOLD, COLOR_BLUE, COLOR_YELLOW, COLOR_RESET); 441 | break; 442 | } 443 | 444 | FD_ZERO(&read_fds); 445 | FD_SET(server_fd, &read_fds); 446 | timeout.tv_sec = 0; 447 | timeout.tv_usec = 100000; 448 | int activity = select(max_fd + 1, &read_fds, NULL, NULL, &timeout); 449 | 450 | if (!server_running) { 451 | break; 452 | } 453 | 454 | if (activity < 0) { 455 | if (errno == EINTR) { 456 | if (!server_running) { 457 | break; 458 | } 459 | continue; 460 | } 461 | fprintf(stderr, "%s%s[ERROR] %sSelect error: %s%s\n", 462 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 463 | break; 464 | } 465 | 466 | time_t current_time = time(NULL); 467 | if (ws_clients && server_running && ws_clients->count > 0 && 468 | difftime(current_time, last_ping_time) * 1000 >= PING_INTERVAL_MS) { 469 | broadcast_to_ws_clients(ws_clients, "ping"); 470 | last_ping_time = current_time; 471 | } 472 | 473 | if (activity > 0 && FD_ISSET(server_fd, &read_fds) && server_running) { 474 | if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) { 475 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 476 | continue; 477 | } 478 | fprintf(stderr, "%s%s[ERROR] %sConnection not accepted: %s%s\n", 479 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET); 480 | continue; 481 | } 482 | 483 | char buffer[BUFFER_SIZE] = {0}; 484 | ssize_t bytes_read = recv(new_socket, buffer, BUFFER_SIZE, MSG_PEEK); 485 | if (bytes_read > 0) { 486 | buffer[bytes_read < BUFFER_SIZE ? bytes_read : BUFFER_SIZE-1] = '\0'; 487 | if (is_websocket_request(buffer)) { 488 | handle_websocket_client(new_socket, ws_clients); 489 | } else { 490 | handle_client(new_socket); 491 | } 492 | } else { 493 | close(new_socket); 494 | } 495 | } 496 | } 497 | 498 | 499 | if (server_fd != -1) { 500 | close(server_fd); 501 | server_fd = -1; 502 | } 503 | cleanup_resources(); 504 | printf("%s%s[SERVER] %s%sServer shut down gracefully%s\n", 505 | BOLD, COLOR_BLUE, BOLD, COLOR_GREEN, COLOR_RESET); 506 | return 0; 507 | } 508 | -------------------------------------------------------------------------------- /src/socket_utils.c: -------------------------------------------------------------------------------- 1 | #include "socket_utils.h" 2 | #include 3 | 4 | int initialize_server(struct sockaddr_in* address) { 5 | int server_fd; 6 | int opt = 1; 7 | int retry_count = 0; 8 | const int MAX_RETRIES = 5; 9 | const int RETRY_DELAY_SEC = 2; 10 | int port = ntohs(address->sin_port); 11 | if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { 12 | perror("Socket failed!"); 13 | return -1; 14 | } 15 | 16 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) != 0) { 17 | perror("setsockopt failed"); 18 | close(server_fd); 19 | return -1; 20 | } 21 | 22 | address->sin_family = AF_INET; 23 | address->sin_addr.s_addr = INADDR_ANY; 24 | 25 | while (retry_count < MAX_RETRIES) { 26 | if (bind(server_fd, (struct sockaddr*)address, sizeof(*address)) < 0) { 27 | if (errno == EADDRINUSE) { 28 | printf("Port %d already in use. Waiting %d seconds before retry (%d/%d)...\n", 29 | port, RETRY_DELAY_SEC, retry_count + 1, MAX_RETRIES); 30 | sleep(RETRY_DELAY_SEC); 31 | retry_count++; 32 | } else { 33 | perror("Bind failed!"); 34 | close(server_fd); 35 | return -1; 36 | } 37 | } else {break;} 38 | } 39 | 40 | if (retry_count >= MAX_RETRIES) { 41 | fprintf(stderr, "Failed to bind after %d attempts. Try manually killing the process using port %d or use a different port.\n", 42 | MAX_RETRIES, port); 43 | close(server_fd); 44 | return -1; 45 | } 46 | if (listen(server_fd, 3) < 0) { 47 | perror("Listen failed!"); 48 | close(server_fd); 49 | return -1; 50 | } 51 | return server_fd; 52 | } 53 | 54 | void read_client_data(int socket, char* buffer) { 55 | ssize_t read_value; 56 | while ((read_value = read(socket, buffer, BUFFER_SIZE)) > 0) { 57 | printf("Client: %s", buffer); 58 | memset(buffer, 0, BUFFER_SIZE); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/sqlite_handler.c: -------------------------------------------------------------------------------- 1 | #include "sqlite_handler.h" 2 | #include 3 | #include "server.h" 4 | #include "debug.h" 5 | 6 | static sqlite3* db = NULL; 7 | static char* db_path = NULL; 8 | static bool db_initialized = false; 9 | 10 | int init_sqlite(const char* path) { 11 | #ifdef DEBUG_MODE 12 | printf("%s%s[DEBUG] %sinit_sqlite() called with path: %s%s\n", 13 | BOLD, COLOR_CYAN, COLOR_RESET, 14 | path ? path : "NULL", COLOR_RESET); 15 | #endif 16 | 17 | if (db != NULL) { 18 | close_sqlite(); 19 | } 20 | 21 | if (db_path != NULL) { 22 | free(db_path); 23 | db_path = NULL; 24 | } 25 | 26 | if (path == NULL || *path == '\0') { 27 | printf("%s%s[SQLite] %s%sNo database path provided%s\n", 28 | BOLD, COLOR_RED, BOLD, COLOR_RESET, COLOR_RESET); 29 | return -1; 30 | } 31 | 32 | db_path = strdup(path); 33 | if (db_path == NULL) { 34 | printf("%s%s[SQLite] %s%sMemory allocation error%s\n", 35 | BOLD, COLOR_RED, BOLD, COLOR_RESET, COLOR_RESET); 36 | return -1; 37 | } 38 | 39 | int rc = sqlite3_open(path, &db); 40 | if (rc != SQLITE_OK) { 41 | printf("%s%s[SQLite] %s%sCannot open database: %s%s\n", 42 | BOLD, COLOR_RED, BOLD, COLOR_RESET, sqlite3_errmsg(db), COLOR_RESET); 43 | sqlite3_close(db); 44 | db = NULL; 45 | return -1; 46 | } 47 | 48 | printf("%s%s[SQLite] %sDatabase initialized: %s%s%s\n", 49 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_CYAN, path, COLOR_RESET); 50 | db_initialized = true; 51 | 52 | #ifdef DEBUG_MODE 53 | printf("%s%s[DEBUG] %sAfter init_sqlite(): db_initialized=%s%s\n", 54 | BOLD, COLOR_CYAN, COLOR_RESET, 55 | db_initialized ? "true" : "false", COLOR_RESET); 56 | #endif 57 | 58 | return 0; 59 | } 60 | 61 | void close_sqlite() { 62 | #ifdef DEBUG_MODE 63 | printf("%s%s[DEBUG] %sclose_sqlite() called, db=%p, db_initialized=%s%s\n", 64 | BOLD, COLOR_CYAN, COLOR_RESET, (void*)db, 65 | db_initialized ? "true" : "false", COLOR_RESET); 66 | #endif 67 | 68 | if (db != NULL) { 69 | sqlite3_close(db); 70 | db = NULL; 71 | printf("%s%s[SQLite] %sDatabase connection closed%s\n", 72 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_RESET); 73 | } 74 | 75 | if (db_path != NULL) { 76 | free(db_path); 77 | db_path = NULL; 78 | } 79 | 80 | db_initialized = false; 81 | 82 | #ifdef DEBUG_MODE 83 | printf("%s%s[DEBUG] %sAfter close_sqlite(): db_initialized=%s%s\n", 84 | BOLD, COLOR_CYAN, COLOR_RESET, 85 | db_initialized ? "true" : "false", COLOR_RESET); 86 | #endif 87 | } 88 | 89 | static int callback(void* data, int argc, char** argv, char** azColName) { 90 | sqlite_result_t* result = (sqlite_result_t*)data; 91 | if (result->column_count == 0) { 92 | result->column_count = argc; 93 | result->columns = malloc(argc * sizeof(char*)); 94 | if (!result->columns) { 95 | fprintf(stderr, "%s%s[SQLite] %sMemory allocation error for columns%s\n", 96 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 97 | return 1; 98 | } 99 | 100 | for (int i = 0; i < argc; i++) { 101 | result->columns[i] = strdup(azColName[i] ? azColName[i] : ""); 102 | } 103 | } 104 | 105 | if (result->row_count >= result->capacity) { 106 | int new_capacity = result->capacity == 0 ? 10 : result->capacity * 2; 107 | char*** new_rows = realloc(result->rows, new_capacity * sizeof(char**)); 108 | if (!new_rows) { 109 | fprintf(stderr, "%s%s[SQLite] %sMemory allocation error for rows%s\n", 110 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 111 | return 1; 112 | } 113 | result->rows = new_rows; 114 | result->capacity = new_capacity; 115 | } 116 | 117 | result->rows[result->row_count] = malloc(argc * sizeof(char*)); 118 | if (!result->rows[result->row_count]) { 119 | fprintf(stderr, "%s%s[SQLite] %sMemory allocation error for row %d%s\n", 120 | BOLD, COLOR_RED, COLOR_RESET, result->row_count, COLOR_RESET); 121 | return 1; 122 | } 123 | 124 | for (int i = 0; i < argc; i++) { 125 | result->rows[result->row_count][i] = strdup(argv[i] ? argv[i] : ""); 126 | } 127 | 128 | result->row_count++; 129 | return 0; 130 | } 131 | 132 | sqlite_result_t* execute_query(const char* query) { 133 | if (!db_initialized || db == NULL) { 134 | fprintf(stderr, "%s%s[SQLite] %sDatabase not initialized%s\n", 135 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 136 | return NULL; 137 | } 138 | 139 | sqlite_result_t* result = malloc(sizeof(sqlite_result_t)); 140 | if (!result) { 141 | fprintf(stderr, "%s%s[SQLite] %sMemory allocation error for result%s\n", 142 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 143 | return NULL; 144 | } 145 | 146 | memset(result, 0, sizeof(sqlite_result_t)); 147 | 148 | char* err_msg = NULL; 149 | printf("%s%s[SQLite] %sExecuting query: %s%s%s\n", 150 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_CYAN, query, COLOR_RESET); 151 | 152 | int rc = sqlite3_exec(db, query, callback, result, &err_msg); 153 | 154 | if (rc != SQLITE_OK) { 155 | fprintf(stderr, "%s%s[SQLite] %s%sSQL error: %s%s\n", 156 | BOLD, COLOR_RED, BOLD, COLOR_RESET, err_msg, COLOR_RESET); 157 | sqlite3_free(err_msg); 158 | free_query_results(result); 159 | return NULL; 160 | } 161 | 162 | return result; 163 | } 164 | 165 | void free_query_results(sqlite_result_t* results) { 166 | if (results == NULL) { 167 | return; 168 | } 169 | 170 | if (results->columns) { 171 | for (int i = 0; i < results->column_count; i++) { 172 | if (results->columns[i]) { 173 | free(results->columns[i]); 174 | } 175 | } 176 | free(results->columns); 177 | } 178 | 179 | if (results->rows) { 180 | for (int i = 0; i < results->row_count; i++) { 181 | if (results->rows[i]) { 182 | for (int j = 0; j < results->column_count; j++) { 183 | if (results->rows[i][j]) { 184 | free(results->rows[i][j]); 185 | } 186 | } 187 | free(results->rows[i]); 188 | } 189 | } 190 | free(results->rows); 191 | } 192 | 193 | free(results); 194 | } 195 | 196 | bool is_db_initialized() { 197 | return db_initialized; 198 | } 199 | 200 | const char* get_db_path() { 201 | return db_path; 202 | } 203 | 204 | void set_db_path(const char* path) { 205 | if (db_path != NULL) { 206 | free(db_path); 207 | db_path = NULL; 208 | } 209 | 210 | if (path != NULL) { 211 | db_path = strdup(path); 212 | printf("%s%s[SQLite] %sDatabase path set to: %s%s%s\n", 213 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_CYAN, db_path, COLOR_RESET); 214 | } else { 215 | printf("%s%s[SQLite] %sDatabase path cleared%s\n", 216 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RESET); 217 | if (db != NULL) { 218 | close_sqlite(); 219 | } 220 | } 221 | } 222 | 223 | char* generate_table_html(sqlite_result_t* result) { 224 | if (!result) { 225 | return strdup("

No query results.

"); 226 | } 227 | 228 | char* html = NULL; 229 | size_t size = 0; 230 | FILE* memfile = open_memstream(&html, &size); 231 | 232 | if (!memfile) { 233 | return strdup("

Error generating results table.

"); 234 | } 235 | 236 | fprintf(memfile, "\n"); 237 | fprintf(memfile, " \n \n"); 238 | for (int i = 0; i < result->column_count; i++) { 239 | fprintf(memfile, " \n", result->columns[i] ? result->columns[i] : ""); 240 | } 241 | fprintf(memfile, " \n \n"); 242 | fprintf(memfile, " \n"); 243 | 244 | if (result->row_count == 0) { 245 | fprintf(memfile, " \n", 246 | result->column_count > 0 ? result->column_count : 1); 247 | } else { 248 | for (int i = 0; i < result->row_count; i++) { 249 | fprintf(memfile, " \n"); 250 | for (int j = 0; j < result->column_count; j++) { 251 | fprintf(memfile, " \n", 252 | result->rows[i][j] ? result->rows[i][j] : ""); 253 | } 254 | fprintf(memfile, " \n"); 255 | } 256 | } 257 | 258 | fprintf(memfile, " \n
%s
No results found
%s
\n"); 259 | 260 | fclose(memfile); 261 | return html; 262 | } 263 | 264 | char* process_sqlite_queries(char* content) { 265 | if (!content || !is_db_initialized()) { 266 | return content; 267 | } 268 | 269 | const char* query_pattern = "\\{\\% query \"([^\"]*)\" \\%\\}"; 270 | regex_t regex; 271 | 272 | if (regcomp(®ex, query_pattern, REG_EXTENDED) != 0) { 273 | fprintf(stderr, "%s%s[SQLite] %sRegex compilation failed%s\n", 274 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 275 | return content; 276 | } 277 | 278 | size_t content_len = strlen(content); 279 | size_t result_capacity = content_len * 2; 280 | char* result = malloc(result_capacity + 1); 281 | 282 | if (!result) { 283 | fprintf(stderr, "%s%s[SQLite] %sMemory allocation error%s\n", 284 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 285 | regfree(®ex); 286 | return content; 287 | } 288 | 289 | size_t result_len = 0; 290 | size_t last_pos = 0; 291 | regmatch_t matches[2]; 292 | 293 | while (regexec(®ex, content + last_pos, 2, matches, 0) == 0) { 294 | size_t start_offset = last_pos + matches[0].rm_so; 295 | size_t end_offset = last_pos + matches[0].rm_eo; 296 | 297 | size_t prefix_len = start_offset - last_pos; 298 | if (prefix_len > 0) { 299 | if (result_len + prefix_len >= result_capacity) { 300 | result_capacity = (result_len + prefix_len) * 2; 301 | char* new_result = realloc(result, result_capacity + 1); 302 | if (!new_result) { 303 | fprintf(stderr, "%s%s[SQLite] %sMemory reallocation error%s\n", 304 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 305 | free(result); 306 | regfree(®ex); 307 | return content; 308 | } 309 | result = new_result; 310 | } 311 | 312 | memcpy(result + result_len, content + last_pos, prefix_len); 313 | result_len += prefix_len; 314 | } 315 | 316 | size_t query_start = last_pos + matches[1].rm_so; 317 | size_t query_end = last_pos + matches[1].rm_eo; 318 | size_t query_len = query_end - query_start; 319 | 320 | char* query = malloc(query_len + 1); 321 | if (!query) { 322 | fprintf(stderr, "%s%s[SQLite] %sMemory allocation error%s\n", 323 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 324 | free(result); 325 | regfree(®ex); 326 | return content; 327 | } 328 | 329 | memcpy(query, content + query_start, query_len); 330 | query[query_len] = '\0'; 331 | sqlite_result_t* query_result = execute_query(query); 332 | free(query); 333 | char* table_html = generate_table_html(query_result); 334 | if (query_result) { 335 | free_query_results(query_result); 336 | } 337 | 338 | size_t html_len = strlen(table_html); 339 | if (result_len + html_len >= result_capacity) { 340 | result_capacity = (result_len + html_len) * 2; 341 | char* new_result = realloc(result, result_capacity + 1); 342 | if (!new_result) { 343 | fprintf(stderr, "%s%s[SQLite] %sMemory reallocation error%s\n", 344 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 345 | free(table_html); 346 | free(result); 347 | regfree(®ex); 348 | return content; 349 | } 350 | result = new_result; 351 | } 352 | 353 | 354 | memcpy(result + result_len, table_html, html_len); 355 | result_len += html_len; 356 | free(table_html); 357 | last_pos = end_offset; 358 | } 359 | 360 | if (last_pos < content_len) { 361 | size_t remaining_len = content_len - last_pos; 362 | 363 | if (result_len + remaining_len >= result_capacity) { 364 | result_capacity = (result_len + remaining_len) * 2; 365 | char* new_result = realloc(result, result_capacity + 1); 366 | if (!new_result) { 367 | fprintf(stderr, "%s%s[SQLite] %sMemory reallocation error%s\n", 368 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET); 369 | free(result); 370 | regfree(®ex); 371 | return content; 372 | } 373 | result = new_result; 374 | } 375 | 376 | memcpy(result + result_len, content + last_pos, remaining_len); 377 | result_len += remaining_len; 378 | } 379 | result[result_len] = '\0'; 380 | regfree(®ex); 381 | return result; 382 | } -------------------------------------------------------------------------------- /template-examples/01-variables.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | {{title}} 13 | 18 | 19 | 20 |

{{title}}

21 | 22 |
23 |

Basic Variable Replacement

24 |

Hello, my name is {{username}}.

25 |

I work at {{company}}.

26 |

Copyright © {{year}}

27 | 28 |

How it works:

29 |
30 |

Define variables in HTML comments:
31 | <!-- template:var username="John Doe" company="Acme Corp" year="2023" -->

32 | 33 |

Then use them with double curly braces:
34 | Hello, my name is {{username}}.

35 |
36 |
37 | 38 |
39 |

Variable Concatenation

40 |

{{username}} from {{company}}.

41 |

This is a {{title}} created in {{year}}.

42 | 43 |

How it works:

44 |
45 |

Variables can be combined with regular text:
46 | {{username}} from {{company}}.

47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /template-examples/02-conditionals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | {{title}} 13 | 20 | 21 | 22 |

{{title}}

23 | 24 |
25 |

Basic If Statement

26 | 27 | {% if is_logged_in %} 28 |

User is logged in.

29 | {% endif %} 30 | 31 | {% if is_admin %} 32 |

User is an admin.

33 | {% else %} 34 |

User is not an admin.

35 | {% endif %} 36 | 37 |

How it works:

38 |
39 |

Define condition variables:
40 | <!-- template:var is_logged_in="1" is_admin="0" -->

41 | 42 |

Basic if statement:
43 | {% if is_logged_in %}
44 |   User is logged in.
45 | {% endif %}

46 | 47 |

If-else statement:
48 | {% if is_admin %}
49 |   User is an admin.
50 | {% else %}
51 |   User is not an admin.
52 | {% endif %}

53 |
54 |
55 | 56 |
57 |

Truthy and Falsy Values

58 |

The following values are considered "true":

59 |
    60 |
  • "1", "true", "yes", "y", "on" and any non-empty string not explicitly false
  • 61 |
62 | 63 |

The following values are considered "false":

64 |
    65 |
  • "0", "false", "no", "n", "off" and empty strings
  • 66 |
67 | 68 |

Examples:

69 |
    70 | {% if has_subscription %} 71 |
  • has_subscription (value: "{{has_subscription}}") is TRUE
  • 72 | {% else %} 73 |
  • has_subscription (value: "{{has_subscription}}") is FALSE
  • 74 | {% endif %} 75 | 76 | {% if status %} 77 |
  • status (value: "{{status}}") is TRUE
  • 78 | {% else %} 79 |
  • status (value: "{{status}}") is FALSE
  • 80 | {% endif %} 81 | 82 | {% if empty_var %} 83 |
  • empty_var (value: "{{empty_var}}") is TRUE
  • 84 | {% else %} 85 |
  • empty_var (value: "{{empty_var}}") is FALSE
  • 86 | {% endif %} 87 |
88 |
89 | 90 |
91 |

Nested Conditions

92 | 93 | {% if is_logged_in %} 94 |

User is logged in.

95 | {% if is_admin %} 96 |

User has admin privileges.

97 | {% else %} 98 |

User has regular privileges.

99 | {% if has_subscription %} 100 |

User has an active subscription.

101 | {% endif %} 102 | {% endif %} 103 | {% else %} 104 |

User is not logged in.

105 | {% endif %} 106 | 107 |

How it works:

108 |
109 |

You can nest conditions inside other conditions:
110 | {% if is_logged_in %}
111 |   User is logged in.
112 |   {% if is_admin %}
113 |     User has admin privileges.
114 |   {% else %}
115 |     User has regular privileges.
116 |   {% endif %}
117 | {% endif %}

118 |
119 |
120 | 121 | -------------------------------------------------------------------------------- /template-examples/03-loops.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{title}} 14 | 20 | 21 | 22 |

{{title}}

23 | 24 |
25 |

Basic Loop

26 | 27 |

Fruit List:

28 |
    29 | {% for item in items %} 30 |
  • {{item}}
  • 31 | {% endfor %} 32 |
33 | 34 |

How it works:

35 |
36 |

Define loop items in an HTML comment:
37 | <!-- template:items "Apple" "Banana" "Cherry" "Date" "Elderberry" -->

38 | 39 |

Basic loop syntax:
40 | {% for item in items %}
41 |   <li>{{item}}</li>
42 | {% endfor %}

43 |
44 |
45 | 46 |
47 |

Loop with Index

48 | 49 |

Numbered List:

50 |
    51 | {% for item in items %} 52 |
  1. {{item}}
  2. 53 | {% endfor %} 54 |
55 | 56 |

Note: The template engine doesn't directly support loop indices, but you can use ordered lists to achieve numbered items.

57 |
58 | 59 |
60 |

Loops with Multi-part Items

61 | 62 | 63 | 64 | 65 |

Product List:

66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {% for item in items %} 74 | 75 | 76 | 77 | 78 | 79 | 80 | {% endfor %} 81 |
NameCategoryColorPrice
{{item.0}}{{item.1}}{{item.2}}${{item.3}}
82 | 83 |

How it works:

84 |
85 |

Define items with pipe-delimited fields:
86 | <!-- template:items "Apple|fruit|red|0.99" "Carrot|vegetable|orange|1.25" ... -->

87 | 88 |

Access individual parts with dot notation:
89 | <td>{{item.0}}</td>
90 | <td>{{item.1}}</td>
91 | <td>{{item.2}}</td>
92 | <td>${{item.3}}</td>

93 |
94 |
95 | 96 |
97 |

Nested Loops

98 | 99 | 100 | 101 | 102 | 103 |

Categorized List:

104 | {% for item in items %} 105 |

{{item.0}}:

106 |
    107 | 108 | 109 |
  • {{item.1}}
  • 110 |
111 | {% endfor %} 112 | 113 |

Note: The template engine doesn't support string splitting, but in a more advanced implementation, 114 | you could have nested loops iterating through comma-separated values.

115 |
116 | 117 | -------------------------------------------------------------------------------- /template-examples/04-conditional-loops.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 22 | 23 | 24 | {{title}} 25 | 37 | 38 | 39 |

{{title}}

40 | 41 |
42 |

Basic Conditional Loop

43 | 44 |

Fruits Only:

45 |
    46 | {% for item in items if item.1 == "fruit" %} 47 |
  • {{item.0}} - ${{item.3}}
  • 48 | {% endfor %} 49 |
50 | 51 |

Vegetables Only:

52 |
    53 | {% for item in items if item.1 == "vegetable" %} 54 |
  • {{item.0}} - ${{item.3}}
  • 55 | {% endfor %} 56 |
57 | 58 |

How it works:

59 |
60 |

Define items with categories and other properties: 61 | <!-- template:items 62 | "Apple|fruit|red|1.99|in stock" 63 | "Banana|fruit|yellow|0.99|in stock" 64 | "Carrot|vegetable|orange|1.49|in stock" 65 | ... 66 | -->

67 | 68 |

Use conditional loops to filter by category: 69 | {% for item in items if item.1 == "fruit" %} 70 | <li>{{item.0}} - ${{item.3}}</li> 71 | {% endfor %}

72 |
73 |
74 | 75 |
76 |

Multiple Filtering Conditions

77 | 78 |

Red Items:

79 |
    80 | {% for item in items if item.2 == "red" %} 81 |
  • {{item.0}} ({{item.1}}) - ${{item.3}}
  • 82 | {% endfor %} 83 |
84 | 85 |

Green Vegetables:

86 |
    87 | {% for item in items if item.1 == "vegetable" if item.2 == "green" %} 88 |
  • {{item.0}} - ${{item.3}}
  • 89 | {% endfor %} 90 |
91 | 92 |

Note: For more complex filtering, you might need to use multiple separate loops or enhance the template engine.

93 |
94 | 95 |
96 |

Filtering with Inequality

97 | 98 |

Items that are NOT vegetables:

99 |
    100 | {% for item in items if item.1 != "vegetable" %} 101 |
  • {{item.0}} ({{item.1}}) - ${{item.3}}
  • 102 | {% endfor %} 103 |
104 |
105 | 106 |
107 |

Real-world Example: Product Availability

108 | 109 |

Product Catalog:

110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | {% for item in items %} 118 | 119 | 120 | 121 | 122 | 123 | 124 | {% endfor %} 125 |
ProductCategoryPriceStatus
{{item.0}}{{item.1}}${{item.3}}{{item.4}}
126 | 127 |

In Stock Items Only:

128 |
    129 | {% for item in items if item.4 == "in stock" %} 130 |
  • {{item.0}} - ${{item.3}}
  • 131 | {% endfor %} 132 |
133 | 134 |

Limited or Sold Out Items:

135 |
    136 | {% for item in items if item.4 != "in stock" %} 137 |
  • {{item.0}} - {{item.4}}
  • 138 | {% endfor %} 139 |
140 |
141 | 142 |
143 |

Advanced Filtering Example

144 | 145 |

Premium Fruits (over $2):

146 |
    147 | {% for item in items if item.1 == "fruit" %} 148 | 149 | 150 |
  • {{item.0}} - ${{item.3}}
  • 151 | {% endfor %} 152 |
153 | 154 |

Note: The current template engine doesn't support numeric comparisons in conditions. 155 | For more advanced filtering, preprocess your data or enhance the template engine.

156 |
157 | 158 | -------------------------------------------------------------------------------- /template-examples/05-sqlite-queries.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | {{title}} 13 | 22 | 23 | 24 |

{{title}}

25 | 26 |
27 |

Note: To use these examples, you must start the server with a SQLite database:

28 |

./bin/blink --database {{db_name}}

29 |

The database should have tables like 'users' and 'products' for these examples to work.

30 |
31 | 32 |
33 |

Basic Query

34 |

This shows all users in the database:

35 | 36 | 37 | {% query "SELECT * FROM users LIMIT 5" %} 38 | 39 |

How it works:

40 |
41 |

Use the query tag with an SQL SELECT statement:
42 | {% query "SELECT * FROM users LIMIT 5" %}

43 | 44 |

The results are automatically formatted as an HTML table.

45 |
46 |
47 | 48 |
49 |

Filtered Query

50 |

This shows users filtered by a condition:

51 | 52 | {% query "SELECT id, name, email FROM users WHERE id > 2 ORDER BY name" %} 53 | 54 |

How it works:

55 |
56 |

Add WHERE clauses for filtering:
57 | {% query "SELECT id, name, email FROM users WHERE id > 2 ORDER BY name" %}

58 |
59 |
60 | 61 |
62 |

Aggregate Functions

63 |

This calculates statistics from the database:

64 | 65 | {% query "SELECT COUNT(*) AS total_users, MIN(id) AS min_id, MAX(id) AS max_id FROM users" %} 66 | 67 |

How it works:

68 |
69 |

Use SQLite's built-in functions:
70 | {% query "SELECT COUNT(*) AS total_users, MIN(id) AS min_id, MAX(id) AS max_id FROM users" %}

71 |
72 |
73 | 74 |
75 |

Join Example

76 |

This joins data from multiple tables:

77 | 78 | {% query "SELECT u.name AS user, p.name AS product FROM users u JOIN products p ON u.id = p.user_id LIMIT 5" %} 79 | 80 |

How it works:

81 |
82 |

Use JOIN to combine tables:
83 | {% query "SELECT u.name AS user, p.name AS product FROM users u JOIN products p ON u.id = p.user_id LIMIT 5" %}

84 |
85 |
86 | 87 |
88 |

Form-Based Queries

89 |

You can use forms to execute SQL operations:

90 | 91 |

Query Form Example:

92 |
93 | 94 | 95 | 96 |
97 | 98 |

How it works:

99 |
100 |

Create a form with action="/sql" and method="POST"
101 | Add a hidden field: name="sql_action" value="select"
102 | Add a textarea with name="sql_query" containing your SQL
103 | The results will be displayed after form submission

104 |
105 |
106 | 107 |
108 |

Insert Data Example

109 |

This form allows inserting new records:

110 | 111 |
112 | 113 | 114 |
115 |

116 | 117 |
118 |

119 | 120 | 121 | 122 | 123 |
124 | 125 |

How it works:

126 |
127 |

Create a form with sql_action="insert"
128 | Use placeholders in brackets [fieldname] in your SQL query
129 | These will be replaced with the form field values

130 |
131 |
132 | 133 |
134 |

Combining with Template Features

135 |

SQL queries can be combined with other template features:

136 | 137 | 138 | 139 | 140 | 141 |

Data Source:

142 | {% query "SELECT COUNT(*) AS total FROM users" %} 143 | 144 | 145 |

Template with Conditional:

146 | {% if has_users %} 147 |

We have {{user_count}} users in the database!

148 | {% else %} 149 |

No users found in the database.

150 | {% endif %} 151 | 152 |

How it works:

153 |
154 |

1. First, define template variables for your conditional logic:
155 | <!-- template:var has_users="1" user_count="12" -->

156 | 157 |

2. You can see the data from a query (in a real app, you would use this to set the variables):
158 | {% query "SELECT COUNT(*) AS total FROM users" %}

159 | 160 |

3. Use template variables in your conditional:
161 | {% if has_users %}
162 |   We have {{user_count}} users in the database!
163 | {% else %}
164 |   No users found in the database.
165 | {% endif %}

166 | 167 |

Note: In production, you would use server-side code to set these variables based on query results.

168 |
169 |
170 | 171 |
172 |

Important Notes

173 |
    174 |
  • SQL queries are executed server-side when the page is rendered
  • 175 |
  • For security, avoid using user input directly in query templates
  • 176 |
  • Use form-based queries with placeholders for user input
  • 177 |
  • SQLite features require a database connection
  • 178 |
  • Tables are automatically styled with the class "sql-table"
  • 179 |
180 |
181 | 182 | -------------------------------------------------------------------------------- /test/create_sample_db.sql: -------------------------------------------------------------------------------- 1 | -- Create tables 2 | CREATE TABLE users ( 3 | id INTEGER PRIMARY KEY, 4 | name TEXT NOT NULL, 5 | email TEXT, 6 | created_at TEXT DEFAULT CURRENT_TIMESTAMP 7 | ); 8 | 9 | CREATE TABLE products ( 10 | id INTEGER PRIMARY KEY, 11 | name TEXT NOT NULL, 12 | description TEXT, 13 | price REAL NOT NULL, 14 | category TEXT, 15 | stock INTEGER DEFAULT 0 16 | ); 17 | 18 | -- Insert sample users 19 | INSERT INTO users (name, email) VALUES 20 | ('John Doe', 'john@example.com'), 21 | ('Jane Smith', 'jane@example.com'), 22 | ('Robert Johnson', 'robert@example.com'), 23 | ('Sarah Williams', 'sarah@example.com'), 24 | ('Michael Brown', 'michael@example.com'); 25 | 26 | -- Insert sample products 27 | INSERT INTO products (name, description, price, category, stock) VALUES 28 | ('Laptop', 'High-performance laptop with 16GB RAM', 999.99, 'Electronics', 10), 29 | ('Smartphone', '6.5-inch display with 128GB storage', 699.99, 'Electronics', 15), 30 | ('Coffee Maker', 'Programmable coffee maker with timer', 49.99, 'Kitchen', 8), 31 | ('Running Shoes', 'Lightweight running shoes, size 9-12', 79.99, 'Sports', 20), 32 | ('Backpack', 'Water-resistant backpack with laptop compartment', 39.99, 'Accessories', 12), 33 | ('Desk Lamp', 'LED desk lamp with adjustable brightness', 29.99, 'Home Office', 18), 34 | ('Wireless Headphones', 'Noise-cancelling wireless headphones', 149.99, 'Electronics', 7); -------------------------------------------------------------------------------- /test/minimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | {{title}} 13 | 49 | 50 | 51 | 52 |

{{title}}

53 | 54 |

This page demonstrates SQLite database functionality with the sample.db database.

55 | 56 |
57 |

Users Table

58 |

Displaying all users from the database:

59 | 60 | {% query "SELECT * FROM users" %} 61 |
62 | 63 |

Create (Insert)

64 |

Add a new user to the database:

65 | 66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 | 82 |
83 |

Delete

84 |

Remove users from the database:

85 | 86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
96 | 97 |
98 | 99 |
100 |

Products Table

101 |

Displaying all products from the database:

102 | 103 | {% query "SELECT * FROM products" %} 104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /test/sample.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter-xD/blink/34b5121d48a820c37d898e23cc9e6c8d8609afd3/test/sample.db -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 18 | 19 | 20 | 21 | {{title}} 22 | 49 | 50 | 51 |

{{title}}

52 | 53 | 54 |
55 |

Basic Variables

56 |

Welcome, {{name}}!

57 |

Logged in: {{logged_in}}

58 |

Admin: {{admin}}

59 |

Subscription: {{subscription}}

60 |

Count: {{count}}

61 |
62 | 63 | 64 |
65 |

Simple Conditions

66 | 67 | {% if logged_in %} 68 |

User is logged in

69 | {% else %} 70 |

User is not logged in

71 | {% endif %} 72 | 73 | {% if admin %} 74 |

User is admin

75 | {% else %} 76 |

User is not admin

77 | {% endif %} 78 | 79 | {% if subscription %} 80 |

Has subscription

81 | {% else %} 82 |

No subscription

83 | {% endif %} 84 |
85 | 86 | 87 |
88 |

Basic Loop

89 |
    90 | {% for item in items %} 91 |
  • {{item}}
  • 92 | {% endfor %} 93 |
94 | 95 |

Loop with Fields

96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | {% for item in items %} 104 | 105 | 106 | 107 | 108 | 109 | 110 | {% endfor %} 111 |
ProductColorPriceStatus
{{item.0}}{{item.1}}{{item.2}}{{item.3}}
112 |
113 | 114 | 115 |
116 |

Conditional Loops

117 |

Only "In stock" items:

118 |
    119 | {% for item in items if item.3 == "In stock" %} 120 |
  • {{item.0}} ({{item.2}})
  • 121 | {% endfor %} 122 |
123 |
124 | 125 | -------------------------------------------------------------------------------- /www/conditional-loops.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 22 | 23 | 24 | {{title}} 25 | 121 | 122 | 123 |

{{title}}

124 | 125 | 133 | 134 |
135 |

Basic Conditional Loop

136 | 137 |

Fruits Only:

138 |
    139 | {% for item in items if item.1 == "fruit" %} 140 |
  • {{item.0}} - ${{item.3}}
  • 141 | {% endfor %} 142 |
143 | 144 |

Vegetables Only:

145 |
    146 | {% for item in items if item.1 == "vegetable" %} 147 |
  • {{item.0}} - ${{item.3}}
  • 148 | {% endfor %} 149 |
150 | 151 |

How it works:

152 |
153 |

Define items with categories and other properties: 154 | <!-- template:items 155 | "Apple|fruit|red|1.99|in stock" 156 | "Banana|fruit|yellow|0.99|in stock" 157 | "Carrot|vegetable|orange|1.49|in stock" 158 | ... 159 | -->

160 | 161 |

Use conditional loops to filter by category: 162 | {% for item in items if item.1 == "fruit" %} 163 | <li>{{item.0}} - ${{item.3}}</li> 164 | {% endfor %}

165 |
166 |
167 | 168 |
169 |

Multiple Filtering Conditions

170 | 171 |

Red Items:

172 |
    173 | {% for item in items if item.2 == "red" %} 174 |
  • {{item.0}} ({{item.1}}) - ${{item.3}}
  • 175 | {% endfor %} 176 |
177 | 178 |

Green Vegetables:

179 |
    180 | {% for item in items if item.1 == "vegetable" if item.2 == "green" %} 181 |
  • {{item.0}} - ${{item.3}}
  • 182 | {% endfor %} 183 |
184 | 185 |

Note: For more complex filtering, you might need to use multiple separate loops or enhance the template engine.

186 |
187 | 188 |
189 |

Filtering with Inequality

190 | 191 |

Items that are NOT vegetables:

192 |
    193 | {% for item in items if item.1 != "vegetable" %} 194 |
  • {{item.0}} ({{item.1}}) - ${{item.3}}
  • 195 | {% endfor %} 196 |
197 |
198 | 199 |
200 |

Real-world Example: Product Availability

201 | 202 |

Product Catalog:

203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | {% for item in items %} 211 | 212 | 213 | 214 | 215 | 216 | 217 | {% endfor %} 218 |
ProductCategoryPriceStatus
{{item.0}}{{item.1}}${{item.3}}{{item.4}}
219 | 220 |

In Stock Items Only:

221 |
    222 | {% for item in items if item.4 == "in stock" %} 223 |
  • {{item.0}} - ${{item.3}}
  • 224 | {% endfor %} 225 |
226 | 227 |

Limited or Sold Out Items:

228 |
    229 | {% for item in items if item.4 != "in stock" %} 230 |
  • {{item.0}} - {{item.4}}
  • 231 | {% endfor %} 232 |
233 |
234 | 235 |
236 |

Advanced Filtering Example

237 | 238 |

Premium Fruits (over $2):

239 |
    240 | {% for item in items if item.1 == "fruit" %} 241 | 242 | 243 |
  • {{item.0}} - ${{item.3}}
  • 244 | {% endfor %} 245 |
246 | 247 |

Note: The current template engine doesn't support numeric comparisons in conditions. 248 | For more advanced filtering, preprocess your data or enhance the template engine.

249 |
250 | 251 | -------------------------------------------------------------------------------- /www/conditionals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{title}} 15 | 201 | 202 | 203 |
204 |
205 |

{{title}}

206 |
207 | 208 |
209 | 217 | 218 |
219 |

Basic If Statement

220 | 221 |

User is logged in (value: {{is_logged_in}})

222 | 223 |

User is not an admin (value: {{is_admin}})

224 | 225 |

How it works:

226 |
227 |

Define condition variables:
228 | <!-- template:var is_logged_in="1" is_admin="0" -->

229 | 230 |

Basic if statement:
231 | {% if is_logged_in %}
232 |   User is logged in.
233 | {% endif %}

234 | 235 |

If-else statement:
236 | {% if is_admin %}
237 |   User is an admin.
238 | {% else %}
239 |   User is not an admin.
240 | {% endif %}

241 |
242 |
243 | 244 |
245 |

Truthy and Falsy Values

246 |

The following values are considered "true":

247 |
    248 |
  • "1", "true", "yes", "y", "on" and any non-empty string not explicitly false
  • 249 |
250 | 251 |

The following values are considered "false":

252 |
    253 |
  • "0", "false", "no", "n", "off" and empty strings
  • 254 |
255 | 256 |

Examples:

257 |
    258 |
  • has_subscription (value: "{{has_subscription}}") is TRUE
  • 259 | 260 |
  • status (value: "{{status}}") is TRUE
  • 261 | 262 |
  • empty_var (value: "{{empty_var}}") is FALSE
  • 263 |
264 |
265 | 266 |
267 |

Nested Conditions

268 | 269 |

User is logged in.

270 |

User has regular privileges.

271 |

User has an active subscription.

272 | 273 |

How it works:

274 |
275 |

You can nest conditions inside other conditions:
276 | {% if is_logged_in %}
277 |   User is logged in.
278 |   {% if is_admin %}
279 |     User has admin privileges.
280 |   {% else %}
281 |     User has regular privileges.
282 |   {% endif %}
283 | {% endif %}

284 |
285 |
286 |
287 |
288 | 289 |
290 |
291 | Blink Template Engine - Conditionals Examples 292 |
293 |
294 | 295 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{server_name}} Server 10 | 313 | 314 | 315 | 316 |
317 |
318 |

{{server_name}}

319 |

Lightweight templating web server with hot reload

320 |
321 |
322 | 323 |
324 |
325 |
326 |

Server Status

327 |
328 |
329 |
330 |
331 |
332 | Name: 333 | {{server_name}} 334 |
335 |
336 | Version: 337 | {{server_version}} 338 |
339 |
340 | Port: 341 | {{port}} 342 |
343 |
344 |
345 |
346 | Templates: 347 | 348 | {% if enable_templates %} 349 | Enabled 350 | {% else %} 351 | Disabled 352 | {% endif %} 353 | 354 |
355 |
356 | Hot Reload: 357 | 358 | Enabled 359 | 360 |
361 |
362 | WebSocket: 363 | 364 | Active at /ws 365 | 366 |
367 |
368 |
369 |
370 |
371 | 372 |
373 |
374 |

Command-Line Usage

375 |
376 |
377 |

Start the server with the following command:

378 |
./bin/blink
379 | 380 |
    381 |
  • 382 | -p, --port PORT 383 | Specify port number (default: 8080) 384 |
  • 385 |
  • 386 | -s, --serve FILE 387 | Specify a custom HTML file to serve 388 |
  • 389 |
  • 390 | -n, --no-templates 391 | Disable template processing 392 |
  • 393 |
  • 394 | -h, --help 395 | Display this help message 396 |
  • 397 |
398 | 399 |
400 |

Examples:

401 |

Default startup (port 8080):

402 |
./bin/blink
403 | 404 |

Use a different port:

405 |
./bin/blink -s my-page.html -p 8081
406 | 407 |

Serve a specific HTML file with templates disabled:

408 |
./bin/blink --serve my-page.html -n
409 |
410 |
411 |
412 | 413 |
414 |
415 |
416 |

Template Examples

417 |
418 |
419 | 426 |
427 |
428 | 429 |
430 |
431 |

Key Features

432 |
433 |
434 |
435 |
🔄
436 |
437 |

Hot Reload

438 |

Automatic browser refresh on HTML file changes

439 |
440 |
441 | 442 |
443 |
📝
444 |
445 |

Templating

446 |

Variables, conditionals, and loops in your HTML

447 |
448 |
449 | 450 |
451 |
📡
452 |
453 |

WebSocket

454 |

Native WebSocket support for real-time updates

455 |
456 |
457 |
458 |
459 |
460 |
461 | 478 | 479 | -------------------------------------------------------------------------------- /www/loops.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{title}} 14 | 105 | 106 | 107 |

{{title}}

108 | 109 | 117 | 118 |
119 |

Basic Loop

120 | 121 |

Fruit List:

122 |
    123 | {% for item in items %} 124 |
  • {{item}}
  • 125 | {% endfor %} 126 |
127 | 128 |

How it works:

129 |
130 |

Define loop items in an HTML comment:
131 | <!-- template:items "Apple" "Banana" "Cherry" "Date" "Elderberry" -->

132 | 133 |

Basic loop syntax:
134 | {% for item in items %}
135 |   <li>{{item}}</li>
136 | {% endfor %}

137 |
138 |
139 | 140 |
141 |

Loop with Index

142 | 143 |

Numbered List:

144 |
    145 | {% for item in items %} 146 |
  1. {{item}}
  2. 147 | {% endfor %} 148 |
149 | 150 |

Note: The template engine doesn't directly support loop indices, but you can use ordered lists to achieve numbered items.

151 |
152 | 153 |
154 |

Loops with Multi-part Items

155 | 156 | 157 | 158 | 159 |

Product List:

160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | {% for item in items %} 168 | 169 | 170 | 171 | 172 | 173 | 174 | {% endfor %} 175 |
NameCategoryColorPrice
{{item.0}}{{item.1}}{{item.2}}${{item.3}}
176 | 177 |

How it works:

178 |
179 |

Define items with pipe-delimited fields:
180 | <!-- template:items "Apple|fruit|red|0.99" "Carrot|vegetable|orange|1.25" ... -->

181 | 182 |

Access individual parts with dot notation:
183 | <td>{{item.0}}</td>
184 | <td>{{item.1}}</td>
185 | <td>{{item.2}}</td>
186 | <td>${{item.3}}</td>

187 |
188 |
189 | 190 |
191 |

Nested Loops

192 | 193 | 194 | 195 | 196 | 197 |

Categorized List:

198 | {% for item in items %} 199 |

{{item.0}}:

200 |
    201 | 202 | 203 |
  • {{item.1}}
  • 204 |
205 | {% endfor %} 206 | 207 |

Note: The template engine doesn't support string splitting, but in a more advanced implementation, 208 | you could have nested loops iterating through comma-separated values.

209 |
210 | 211 | -------------------------------------------------------------------------------- /www/sample.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter-xD/blink/34b5121d48a820c37d898e23cc9e6c8d8609afd3/www/sample.db -------------------------------------------------------------------------------- /www/sql.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | SQLite Basic CRUD 14 | 324 | 325 | 326 |
327 |
328 |

{{title}}

329 |
330 | 331 |
332 | 340 | 341 |
342 |

Current Data

343 |

Current content of the users table:

344 | {% query "SELECT * FROM posts" %} 345 |
346 | 347 |
348 |

Create (Insert)

349 |

Add a new user to the database:

350 | 351 |
352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 |
367 | 368 |

How it works:

369 |
370 |

This form submits an SQL INSERT statement:
371 | INSERT INTO users (name, email, age) VALUES ('[name]', '[email]', [age])

372 | 373 |

The form field values replace the placeholders in brackets.

374 |
375 |
376 | 377 |
378 |

Read (Select)

379 |

Query database information:

380 | 381 |

Users 18 and Older

382 | {% query "SELECT COUNT(*) AS total_users FROM users" %} 383 | 384 |
385 |

Use the {% query %} template tag to run SQL queries:
386 | {% query "SELECT COUNT(*) AS total_users FROM users" %}

387 |
388 |
389 | 390 |
391 |

Update

392 |

Modify existing user data:

393 | 394 |
395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 |
413 | 414 |

How it works:

415 |
416 |

This form generates an SQL UPDATE statement:
417 | UPDATE users SET name='[name]', email='[email]', age=[age] WHERE id=[id]

418 |
419 |
420 | 421 |
422 |

Delete

423 |

Remove users from the database:

424 | 425 |
426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 |
435 | 436 |

How it works:

437 |
438 |

This form runs a DELETE statement:
439 | DELETE FROM users WHERE id=[id]

440 |
441 |
442 | 443 |
444 |

SQL Templates Guide

445 |

Blink Server supports embedding SQL queries directly in your HTML files using templates:

446 | 447 |

Basic Query Syntax

448 |
449 |

To execute an SQL query and display its results in a table, use:
450 | {% query "SELECT * FROM table_name" %}

451 |
452 | 453 |

Example Result

454 |

This shows all posts with their titles and content:

455 | {% query "SELECT id, title, content FROM posts LIMIT 3" %} 456 | 457 |

Aggregate Functions

458 |
459 |

You can use SQL aggregate functions like COUNT, AVG, SUM:
460 | {% query "SELECT COUNT(*) AS count FROM users" %}

461 |
462 | 463 |

Joining Tables

464 |
465 |

For related data, you can join tables:
466 | {% query "SELECT p.title, u.name FROM posts p JOIN users u ON p.user_id = u.id" %}

467 |
468 | 469 |

Example Result

470 |

This shows posts with their authors:

471 | {% query "SELECT p.title, u.name AS author FROM posts p LEFT JOIN users u ON p.user_id = u.id LIMIT 3" %} 472 | 473 |

Important Notes

474 |
    475 |
  • The query must be a valid SQL statement
  • 476 |
  • Results are automatically formatted as HTML tables
  • 477 |
  • For security, avoid using user input directly in queries
  • 478 |
  • Keep queries simple for better performance
  • 479 |
480 |
481 |
482 |
483 | 484 |
485 |
486 | Blink Server SQLite CRUD Examples 487 |
488 |
489 | 490 | -------------------------------------------------------------------------------- /www/variables.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{title}} 15 | 209 | 210 | 211 |
212 |
213 |

{{title}}

214 |
215 | 216 |
217 | 225 | 226 |
227 |

Basic Variable Replacement

228 |

Hello, my name is {{username}}.

229 |

I work at {{company}}.

230 |

Copyright © {{year}}

231 | 232 |

How it works:

233 |
234 |

Define variables in HTML comments:
235 | <!-- template:var username="John Doe" company="Acme Corp" year="2023" -->

236 | 237 |

Then use them with double curly braces:
238 | Hello, my name is {{username}}.

239 |
240 |
241 | 242 |
243 |

Variable Concatenation

244 |

{{username}} from {{company}}.

245 |

This is a {{title}} created in {{year}}.

246 | 247 |

How it works:

248 |
249 |

Variables can be combined with regular text:
250 | {{username}} from {{company}}.

251 |
252 |
253 |
254 |
255 | 256 | 261 | 262 | --------------------------------------------------------------------------------