rewrite URL shortener in Rust

This commit is contained in:
2026-07-10 04:46:34 +00:00
parent a250ce4c1d
commit a6e8be7553
16 changed files with 2906 additions and 1102 deletions
+1452
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
use std::io::Read;
use std::sync::Arc;
use tiny_http::{Header, Response, Server, StatusCode};
use ushort::{App, Config, RequestData};
fn main() {
let arguments: Vec<String> = std::env::args().collect();
if arguments.len() != 2 {
eprintln!("Usage: {} <config.toml|config.json>", arguments[0]);
std::process::exit(1);
}
let config = Config::load(&arguments[1]).unwrap_or_else(|error| {
eprintln!("Configuration error: {error}");
std::process::exit(1);
});
let address = format!("{}:{}", config.host, config.port);
let app = App::new(config).unwrap_or_else(|error| {
eprintln!("Startup error: {error}");
std::process::exit(1);
});
let server = Arc::new(Server::http(&address).unwrap_or_else(|error| {
eprintln!("Failed to listen on {address}: {error}");
std::process::exit(1);
}));
let shutdown_server = Arc::clone(&server);
ctrlc::set_handler(move || shutdown_server.unblock()).unwrap_or_else(|error| {
eprintln!("Failed to install signal handler: {error}");
std::process::exit(1);
});
eprintln!("URL Shortener listening on http://{address}");
eprintln!("Base URL : {}", app.config.base_url);
eprintln!(
"Base path : {}",
if app.config.base_path.is_empty() {
"/"
} else {
&app.config.base_path
}
);
eprintln!("DB path : {}", app.config.db_path.display());
eprintln!(
"Short codes : {}{} chars",
app.config.min_short_length, app.config.max_short_length
);
eprintln!("Retention days : {}", app.config.retention_days);
eprintln!(
"Rate limit : {} req/{}s per IP",
app.config.rate_limit_requests, app.config.rate_limit_window
);
eprintln!("Production : {}", app.config.production);
for mut request in server.incoming_requests() {
let maximum_body = app.max_request_body_bytes();
let declared_too_large = request
.body_length()
.is_some_and(|length| length > maximum_body);
let mut body = Vec::new();
if !declared_too_large {
let mut reader = request
.as_reader()
.take(maximum_body.saturating_add(1) as u64);
if let Err(error) = reader.read_to_end(&mut body) {
eprintln!("Failed to read request body: {error}");
continue;
}
}
let body_too_large = declared_too_large || body.len() > maximum_body;
let data = RequestData {
method: request.method().as_str().to_string(),
target: request.url().to_string(),
headers: request
.headers()
.iter()
.map(|header| {
(
header.field.as_str().to_string(),
header.value.as_str().into(),
)
})
.collect(),
body,
remote_ip: request
.remote_addr()
.map(|address| address.ip().to_string())
.unwrap_or_default(),
};
let client = data.remote_ip.clone();
let method = data.method.clone();
let target = data
.target
.split_once('?')
.map_or_else(|| data.target.clone(), |(path, _)| path.to_string());
let result = if body_too_large {
ushort::ResponseData::payload_too_large()
} else {
app.handle(data)
};
let status = result.status;
let mut response = Response::from_data(result.body).with_status_code(StatusCode(status));
for (name, value) in result.headers {
match Header::from_bytes(name.as_bytes(), value.as_bytes()) {
Ok(header) => response.add_header(header),
Err(()) => eprintln!("Skipped invalid response header {name}"),
}
}
if let Err(error) = request.respond(response) {
eprintln!("Failed to respond to {client}: {error}");
} else {
eprintln!("{client} - \"{method} {target}\" {status}");
}
}
eprintln!("Shutting down.");
}