Files
guara/crates/guara-cli/src/serve.rs
T
Lucas RayanandClaude Sonnet 5 6328220c60
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
chore(i18n): finish translating code comments and scripts to English
Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:43:02 -03:00

158 lines
6.2 KiB
Rust

//! Local UI server (127.0.0.1 + token): serves the same UI as the Tauri app.
use guara_core::service::Engine;
use serde_json::json;
use std::io::Read;
use std::sync::Arc;
use tiny_http::{Header, Method, Request, Response, Server};
const INDEX: &str = include_str!("../../../apps/gui/ui/index.html");
const CSS: &str = include_str!("../../../apps/gui/ui/app.css");
const JS: &str = include_str!("../../../apps/gui/ui/app.js");
const PREVIEW_JS: &str = include_str!("../../../apps/gui/ui/preview.js");
const I18N_JS: &str = include_str!("../../../apps/gui/ui/i18n.js");
const I18N_EN_JS: &str = include_str!("../../../apps/gui/ui/i18n-en.js");
const EXPLORER_JS: &str = include_str!("../../../apps/gui/ui/explorer.js");
const LOGO: &[u8] = include_bytes!("../../../logo.png");
const PDF_JS: &str = include_str!("../../../apps/gui/ui/vendor/pdf.min.mjs");
const PDF_WORKER: &str = include_str!("../../../apps/gui/ui/vendor/pdf.worker.min.mjs");
pub fn run(port: u16, open: bool) -> Result<(), String> {
let server = Server::http(("127.0.0.1", port)).map_err(|e| e.to_string())?;
let real_port = server
.server_addr()
.to_ip()
.map(|a| a.port())
.unwrap_or(port);
let mut tok = [0u8; 16];
getrandom::fill(&mut tok).map_err(|e| e.to_string())?;
let token: Arc<str> = tok
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
.into();
let url = format!("http://127.0.0.1:{real_port}/?token={token}");
eprintln!("Guará UI em {url}\n(Ctrl+C para encerrar. Para ler discos brutos, rode como administrador/root.)");
if open {
open_browser(&url);
}
let eng = Arc::new(Engine::new());
let hosts: Arc<[String; 2]> = Arc::new([
format!("127.0.0.1:{real_port}"),
format!("localhost:{real_port}"),
]);
for req in server.incoming_requests() {
let (eng, token, hosts) = (eng.clone(), token.clone(), hosts.clone());
// One thread per request: streaming video/audio must not block the UI.
std::thread::spawn(move || handle(req, &eng, &token, &hosts[..]));
}
Ok(())
}
fn header(name: &str, value: &str) -> Header {
Header::from_bytes(name.as_bytes(), value.as_bytes()).unwrap()
}
fn handle(req: Request, eng: &Engine, token: &str, hosts: &[String]) {
let host_ok = req
.headers()
.iter()
.any(|h| h.field.equiv("Host") && hosts.iter().any(|x| x == h.value.as_str()));
if !host_ok {
let _ = req.respond(Response::from_string("host inválido").with_status_code(403));
return;
}
let Some(mut req) = guara_core::http::handle_media(eng, token, req) else {
return;
};
let full = req.url().to_string();
let (path, _query) = full.split_once('?').unwrap_or((full.as_str(), ""));
let ct = |v: &str| header("Content-Type", v);
match (req.method().clone(), path) {
(Method::Get, "/") | (Method::Get, "/index.html") => {
let _ = req
.respond(Response::from_string(INDEX).with_header(ct("text/html; charset=utf-8")));
}
(Method::Get, "/app.css") => {
let _ =
req.respond(Response::from_string(CSS).with_header(ct("text/css; charset=utf-8")));
}
(Method::Get, "/app.js") => {
let _ = req.respond(
Response::from_string(JS).with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Get, "/preview.js") => {
let _ = req.respond(
Response::from_string(PREVIEW_JS).with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Get, "/i18n.js") => {
let _ = req.respond(
Response::from_string(I18N_JS).with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Get, "/i18n-en.js") => {
let _ = req.respond(
Response::from_string(I18N_EN_JS).with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Get, "/logo.png") => {
let _ = req.respond(Response::from_data(LOGO).with_header(ct("image/png")));
}
(Method::Get, "/explorer.js") => {
let _ = req.respond(
Response::from_string(EXPLORER_JS)
.with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Get, "/vendor/pdf.min.mjs") => {
let _ = req.respond(
Response::from_string(PDF_JS).with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Get, "/vendor/pdf.worker.min.mjs") => {
let _ = req.respond(
Response::from_string(PDF_WORKER).with_header(ct("text/javascript; charset=utf-8")),
);
}
(Method::Post, p) if p.starts_with("/rpc/") => {
let ok = req
.headers()
.iter()
.any(|h| h.field.equiv("X-Guara-Token") && h.value.as_str() == token);
if !ok {
let _ = req.respond(Response::from_string("token inválido").with_status_code(401));
return;
}
let method = p.trim_start_matches("/rpc/").to_string();
let mut body = String::new();
let _ = req.as_reader().take(1 << 20).read_to_string(&mut body);
let params = serde_json::from_str(&body).unwrap_or(json!({}));
let (code, out) = match eng.rpc(&method, params) {
Ok(v) => (200, v),
Err(e) => (400, json!({ "error": e.to_string() })),
};
let _ = req.respond(
Response::from_string(out.to_string())
.with_status_code(code)
.with_header(ct("application/json")),
);
}
_ => {
let _ = req.respond(Response::from_string("não encontrado").with_status_code(404));
}
}
}
fn open_browser(url: &str) {
#[cfg(target_os = "linux")]
let _ = std::process::Command::new("xdg-open").arg(url).spawn();
#[cfg(target_os = "macos")]
let _ = std::process::Command::new("open").arg(url).spawn();
#[cfg(windows)]
let _ = std::process::Command::new("cmd")
.args(["/C", "start", "", url])
.spawn();
}