feat: pré-visualização completa, tema, nova identidade e correções (0.2.0)
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
- Preview de imagem, vídeo/MP4, áudio, PDF (pdf.js), texto, Office/ODF, ZIP e hex, lido sob demanda com Range; tela cheia com navegação por teclado. - Seletor de tema Auto/Claro/Escuro persistido (vale também como administrador). - Identidade verde-azulada com logo de pixels que formam um lobo. - Corrige reinício como administrador no AppImage (usa $APPIMAGE) e mostra o motivo. - Corrige barra inferior piscando (render idempotente, barra criada uma vez). - Corrige FAT32 em mídias pequenas não reconhecido. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
510f1bc48e
commit
c5c94a9956
32 files changed
+1666
-227
No files matched your search
+135
-62
@@ -3,12 +3,17 @@
|
||||
use guara_core::service::Engine;
|
||||
use serde_json::json;
|
||||
use std::io::Read;
|
||||
use tiny_http::{Header, Method, Response, Server};
|
||||
use std::sync::Arc;
|
||||
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
|
||||
|
||||
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 LOGO: &str = include_str!("../../../apps/gui/ui/guara-logo.svg");
|
||||
const ICON: &str = include_str!("../../../apps/gui/ui/guara.svg");
|
||||
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())?;
|
||||
@@ -19,78 +24,146 @@ pub fn run(port: u16, open: bool) -> Result<(), String> {
|
||||
.unwrap_or(port);
|
||||
let mut tok = [0u8; 16];
|
||||
getrandom::getrandom(&mut tok).map_err(|e| e.to_string())?;
|
||||
let token: String = tok.iter().map(|b| format!("{b:02x}")).collect();
|
||||
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 = Engine::new();
|
||||
let hosts = [
|
||||
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 mut req in server.incoming_requests() {
|
||||
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));
|
||||
continue;
|
||||
}
|
||||
let path = req.url().split('?').next().unwrap_or("/").to_string();
|
||||
let ct = |v: &str| Header::from_bytes("Content-Type", v).unwrap();
|
||||
match (req.method().clone(), path.as_str()) {
|
||||
(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, "/guara.svg") => {
|
||||
let _ = req.respond(Response::from_string(ICON).with_header(ct("image/svg+xml")));
|
||||
}
|
||||
(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));
|
||||
continue;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
]);
|
||||
for req in server.incoming_requests() {
|
||||
let (eng, token, hosts) = (eng.clone(), token.clone(), hosts.clone());
|
||||
// Uma thread por requisição: vídeo/áudio em streaming não pode travar a interface.
|
||||
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(mut 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 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, "/guara-logo.svg") => {
|
||||
let _ = req.respond(Response::from_string(LOGO).with_header(ct("image/svg+xml")));
|
||||
}
|
||||
(Method::Get, "/guara.svg") => {
|
||||
let _ = req.respond(Response::from_string(ICON).with_header(ct("image/svg+xml")));
|
||||
}
|
||||
(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::Get, p) if p.starts_with("/file/") => {
|
||||
let tok_ok = query
|
||||
.split('&')
|
||||
.any(|kv| kv.strip_prefix("t=") == Some(token));
|
||||
if !tok_ok {
|
||||
let _ = req.respond(Response::from_string("token inválido").with_status_code(401));
|
||||
return;
|
||||
}
|
||||
let mut it = p.trim_start_matches("/file/").split('/');
|
||||
let (job, id) = (
|
||||
it.next().unwrap_or(""),
|
||||
it.next().and_then(|s| s.parse::<u64>().ok()),
|
||||
);
|
||||
let range = req
|
||||
.headers()
|
||||
.iter()
|
||||
.find(|h| h.field.equiv("Range"))
|
||||
.map(|h| h.value.as_str().to_string());
|
||||
let Some(id) = id else {
|
||||
let _ = req.respond(Response::from_string("id inválido").with_status_code(400));
|
||||
return;
|
||||
};
|
||||
match eng.file_slice(job, id, range.as_deref()) {
|
||||
Ok(slice) => {
|
||||
let (status, len) = (slice.status(), slice.len() as usize);
|
||||
let headers: Vec<Header> =
|
||||
slice.headers().iter().map(|(k, v)| header(k, v)).collect();
|
||||
let resp =
|
||||
Response::new(StatusCode(status), headers, slice.reader(), Some(len), None);
|
||||
let _ = req.respond(resp);
|
||||
}
|
||||
Err(code) => {
|
||||
let _ =
|
||||
req.respond(Response::from_string("indisponível").with_status_code(code));
|
||||
}
|
||||
}
|
||||
}
|
||||
(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();
|
||||
|
||||
Reference in new issue
Block a user