Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
**/*.rs.bk
.something/
7 changes: 7 additions & 0 deletions mirdb-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub struct Config {
pub l0_compaction_trigger: usize,

pub thread_sleep_ms: usize,

pub homepage_addr: Option<String>,
}

impl Config {
Expand All @@ -44,6 +46,11 @@ impl Config {
opt.thread_sleep_ms = self.thread_sleep_ms;
Ok(opt)
}

/// Returns the homepage server address, or a default if not configured
pub fn homepage_server_addr(&self) -> String {
self.homepage_addr.clone().unwrap_or_else(|| "0.0.0.0:8080".to_string())
}
}

fn to_size_unit(x: &[u8]) -> usize {
Expand Down
42 changes: 42 additions & 0 deletions mirdb-server/src/homepage/assets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Static asset management for the MirDB homepage.
* Owner: Shared - First Builder
*
* Embeds CSS and image files into the binary at compile time.
*
* Expected exports:
* - styles_css() -> &'static str: Returns the embedded CSS content
* - logo_data() -> &'static [u8]: Returns the embedded logo image bytes
* - content_type(path) -> &'static str: Maps file extensions to MIME types
*/

/// Returns the embedded CSS content
pub fn styles_css() -> &'static str {
include_str!("../../static/styles.css")
}

/// Returns the embedded logo image bytes
pub fn logo_data() -> &'static [u8] {
include_bytes!("../../../assets/logo.gif")
}

/// Maps file extensions to MIME types
pub fn content_type(path: &str) -> &'static str {
if path.ends_with(".css") {
"text/css"
} else if path.ends_with(".html") || path.ends_with(".htm") {
"text/html"
} else if path.ends_with(".js") {
"application/javascript"
} else if path.ends_with(".png") {
"image/png"
} else if path.ends_with(".jpg") || path.ends_with(".jpeg") {
"image/jpeg"
} else if path.ends_with(".gif") {
"image/gif"
} else if path.ends_with(".svg") {
"image/svg+xml"
} else {
"application/octet-stream"
}
}
47 changes: 47 additions & 0 deletions mirdb-server/src/homepage/html.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* HTML template generation for the MirDB homepage.
* Owner: Shared - First Builder
*
* This module generates or embeds the homepage HTML.
* Uses include_str! or string literals to serve the page.
*
* Expected exports:
* - homepage_html() -> String: Returns the complete homepage HTML document
* - hero_section() -> String: Returns the hero section HTML fragment
* - features_section() -> String: Returns the features section HTML fragment
*/

/// Returns the complete homepage HTML document
pub fn homepage_html() -> String {
include_str!("../../static/index.html").to_string()
}

/// Returns the hero section HTML fragment
pub fn hero_section() -> String {
r#"
<section id="hero">
<h1>Welcome to MirDB</h1>
<p>A persistent key-value store with memcached protocol support</p>
</section>
"#.to_string()
}

/// Returns the features section HTML fragment
pub fn features_section() -> String {
r#"
<section id="features">
<div class="feature">
<h3>High Performance</h3>
<p>Built on efficient data structures for maximum throughput</p>
</div>
<div class="feature">
<h3>Persistent Storage</h3>
<p>Your data survives restarts with durable LSM-tree storage</p>
</div>
<div class="feature">
<h3>Memcached Compatible</h3>
<p>Drop-in replacement compatible with existing memcached clients</p>
</div>
</section>
"#.to_string()
}
101 changes: 101 additions & 0 deletions mirdb-server/src/homepage/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Homepage module for MirDB.
*
* This file is created by the first scenario builder and
* serves as the entry point for the homepage feature.
*
* Expected exports:
* - serve_homepage() -> Response: Returns the homepage HTML response
* - serve_static(path) -> Response: Serves embedded CSS and image assets
*/

use std::io::{Read, Write};
use std::net::TcpStream;

pub mod assets;
pub mod html;

/// HTTP response status codes
pub const HTTP_OK: &str = "HTTP/1.1 200 OK";
pub const HTTP_NOT_FOUND: &str = "HTTP/1.1 404 Not Found";
pub const HTTP_METHOD_NOT_ALLOWED: &str = "HTTP/1.1 405 Method Not Allowed";

/// Serve the homepage HTML document
pub fn serve_homepage() -> String {
let body = html::homepage_html();
format!(
"{}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
HTTP_OK,
body.len(),
body
)
}

/// Serve static assets (CSS, images) by path
pub fn serve_static(path: &str) -> String {
match path {
"/styles.css" => {
let body = assets::styles_css();
format!(
"{}\r\nContent-Type: text/css; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
HTTP_OK,
body.len(),
body
)
}
_ => {
let body = "Not Found";
format!(
"{}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
HTTP_NOT_FOUND,
body.len(),
body
)
}
}
}

/// Handle an incoming HTTP request and write the response to the stream
pub fn handle_request(stream: &mut TcpStream) {
let mut buffer = [0u8; 4096];
match stream.read(&mut buffer) {
Ok(n) if n > 0 => {
let request = String::from_utf8_lossy(&buffer[..n]);
let response = route_request(&request);
let _ = stream.write_all(response.as_bytes());
}
_ => {}
}
}

/// Route an HTTP request to the appropriate handler
fn route_request(request: &str) -> String {
let first_line = request.lines().next().unwrap_or("");
let parts: Vec<&str> = first_line.split_whitespace().collect();

if parts.len() < 2 {
return format_error_response(HTTP_NOT_FOUND, "Not Found");
}

let method = parts[0];
let path = parts[1];

if method != "GET" {
return format_error_response(HTTP_METHOD_NOT_ALLOWED, "Method Not Allowed");
}

match path {
"/" => serve_homepage(),
"/styles.css" => serve_static(path),
_ => format_error_response(HTTP_NOT_FOUND, "Not Found"),
}
}

fn format_error_response(status: &str, body: &str) -> String {
format!(
"{}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
status,
body.len(),
body
)
}
2 changes: 2 additions & 0 deletions mirdb-server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod homepage;
pub mod web_server;
8 changes: 8 additions & 0 deletions mirdb-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ mod test_utils;
mod thread_pool;
mod types;
mod wal;
mod homepage;
mod web_server;

pub struct Server {
store: Arc<Store>,
Expand Down Expand Up @@ -127,6 +129,12 @@ Welcome to MirDB!
.trim_matches('\n')
);

// Start the HTTP homepage server
let homepage_addr = conf.homepage_server_addr();
if let Ok(homepage_socket) = homepage_addr.parse() {
let _ = web_server::start_http_server(homepage_socket);
}

serve(addr, move || Ok(Server::new(store.clone())));

Ok(())
Expand Down
10 changes: 5 additions & 5 deletions mirdb-server/src/parser_util/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ macro_rules! chain {
chain!(@inner $i, $mac!($($args)*))
);
(@inner $i:expr, $e:ident >> $($rest:tt)*) => (
chain!(@inner $i, call!($e) >> $($rest)*);
chain!(@inner $i, call!($e) >> $($rest)*)
);
(@inner $i:expr, $mac:ident!($($args:tt)*) >> $($rest:tt)*) => ({
use $crate::parser_util::macros::IRResult;
Expand All @@ -187,7 +187,7 @@ macro_rules! chain {
}
});
(@inner $i:expr, $field:ident : $e:ident >> $($rest:tt)*) => (
chain!(@inner $i, $field: call!($e) >> $($rest)*);
chain!(@inner $i, $field: call!($e) >> $($rest)*)
);
(@inner $i:expr, $field:ident : $mac:ident!($($args:tt)*) >> $($rest:tt)*) => ({
use $crate::parser_util::macros::IRResult;
Expand All @@ -201,7 +201,7 @@ macro_rules! chain {
}
});
(@inner $i:expr, $e:ident >> ($($rest:tt)*)) => (
chain!(@inner $i, call!($e) >> ($($rest)*));
chain!(@inner $i, call!($e) >> ($($rest)*))
);
(@inner $i:expr, $mac:ident!($($args:tt)*) >> ($($rest:tt)*)) => ({
use $crate::parser_util::macros::IRResult;
Expand All @@ -215,7 +215,7 @@ macro_rules! chain {
}
});
(@inner $i:expr, $field:ident : $e:ident >> ( $($rest:tt)* )) => (
chain!(@inner $i, $field: call!($e) >> ( $($rest)* ) );
chain!(@inner $i, $field: call!($e) >> ( $($rest)* ))
);
(@inner $i:expr, $field:ident : $mac:ident!( $($args:tt)* ) >> ( $($rest:tt)* )) => ({
use $crate::parser_util::macros::IRResult;
Expand All @@ -241,7 +241,7 @@ macro_rules! chain {
chain!(@inner $i, $($rest)*)
);
($e:ident! >> $($rest:tt)* ) => (
chain!(call!($e) >> $($rest)*);
chain!(call!($e) >> $($rest)*)
);
}

Expand Down
42 changes: 42 additions & 0 deletions mirdb-server/src/web_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* HTTP web server for serving the MirDB homepage.
* Owner: Scenario 1 - Homepage HTTP Endpoint
*
* Sets up an HTTP listener that serves the homepage and static assets.
* Uses a lightweight threaded TCP server to avoid conflicts with tokio 0.1.
*
* Expected exports:
* - start_http_server(addr, homepage_handler) -> Result: Binds HTTP server to address
* - HomepageService: Service implementation for HTTP requests
* - route_request(path) -> Response: Routes incoming requests to appropriate handlers
*/

use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::thread;

use crate::homepage;

/// Starts the HTTP server on the given address.
/// Spawns a new thread so the server runs concurrently with the memcached server.
pub fn start_http_server(addr: SocketAddr) -> std::io::Result<()> {
let listener = TcpListener::bind(addr)?;
println!("HTTP server listening on http://{}", addr);

thread::spawn(move || {
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
thread::spawn(move || {
homepage::handle_request(&mut stream);
});
}
Err(e) => {
eprintln!("HTTP connection error: {}", e);
}
}
}
});

Ok(())
}
48 changes: 48 additions & 0 deletions mirdb-server/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<!--
MirDB Product Homepage
Created by: First scenario builder
Modified by: Scenarios 2-6, 8, 10

This is the main homepage HTML template. Each scenario adds
its assigned sections while preserving existing structure.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MirDB - Persistent Key-Value Store</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<!-- Header/Nav: Owned by Scenario 5 -->
<header>...</header>

<!-- Hero: Owned by Scenario 2 -->
<!-- CTA: Owned by Scenario 4 - inside hero for above-the-fold visibility -->
<section id="hero">
<div class="hero-content">
<h1>MirDB</h1>
<p class="tagline">A persistent key-value store written in Rust</p>
<a href="https://github.com/yetone/mirdb" class="cta-button" role="button">Get Started</a>
</div>
</section>

<!-- Features: Owned by Scenario 3 -->
<section id="features">...</section>

<!-- CTA: Owned by Scenario 4 -->
<section id="cta">...</section>

<!-- Footer: Owned by Scenario 6 -->
<footer class="site-footer">
<div class="footer-content">
<p class="copyright">&copy; 2026 MirDB. All rights reserved.</p>
<nav class="footer-links" aria-label="Footer navigation">
<a href="https://github.com/yetone/mirdb" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="https://github.com/yetone/mirdb/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">License</a>
</nav>
</div>
</footer>
</body>
</html>
Loading