feat: Guará 0.1.0 — recuperação de arquivos (núcleo, CLI e interface gráfica)
Núcleo em Rust somente leitura: NTFS, FAT, exFAT, partições MBR/GPT e perdidas, carving por estrutura, fusão sem duplicatas, confiança por arquivo, imagem forense, pausar/retomar. CLI `guara`, UI web/Tauri, CI e releases por tag. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
765cb0d4a2
57 files changed
+8621
No files matched your search
@@ -0,0 +1,519 @@
|
||||
//! Guará CLI: `guara devices | inspect | scan | resume | recover | image | ui`.
|
||||
|
||||
mod serve;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use guara_core::service::{Engine, Query};
|
||||
use guara_core::{Category, Confidence, ScanMode, ScanOptions};
|
||||
use serde_json::{json, Value};
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "guara", version, about = "Guará — recuperação de arquivos (somente leitura na origem)", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Lista discos e pendrives detectados
|
||||
Devices {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Mostra partições, sistemas de arquivos, espaço não particionado e volumes cifrados
|
||||
Inspect {
|
||||
/// Dispositivo (ex.: /dev/sdb, \\.\PhysicalDrive1) ou arquivo de imagem
|
||||
path: String,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Procura arquivos apagados/perdidos (metadados + assinatura)
|
||||
Scan(ScanArgs),
|
||||
/// Continua uma varredura pausada (Ctrl+C) a partir do arquivo de sessão
|
||||
Resume {
|
||||
session: String,
|
||||
#[command(flatten)]
|
||||
out: OutArgs,
|
||||
},
|
||||
/// Recupera arquivos de uma sessão salva
|
||||
Recover {
|
||||
session: String,
|
||||
/// Pasta de destino (em OUTRO disco)
|
||||
dest: String,
|
||||
#[command(flatten)]
|
||||
filter: FilterArgs,
|
||||
},
|
||||
/// Cria uma imagem do disco (tolerante a setores ruins) para trabalhar na cópia
|
||||
Image { path: String, dest: String },
|
||||
/// Abre a interface gráfica no navegador (modo local)
|
||||
Ui {
|
||||
#[arg(long, default_value_t = 0)]
|
||||
port: u16,
|
||||
#[arg(long)]
|
||||
no_open: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Clone)]
|
||||
struct FilterArgs {
|
||||
/// Categorias: image, document, video, audio, archive, database, other
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
category: Vec<String>,
|
||||
/// Confiança mínima: low, medium, high
|
||||
#[arg(long, default_value = "low")]
|
||||
min_confidence: String,
|
||||
/// Só nomes/caminhos que contenham este texto
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Clone)]
|
||||
struct OutArgs {
|
||||
/// Salva a sessão (necessária para recover/resume)
|
||||
#[arg(long)]
|
||||
session: Option<String>,
|
||||
/// Lista os arquivos encontrados
|
||||
#[arg(long)]
|
||||
list: bool,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
struct ScanArgs {
|
||||
/// Dispositivo ou imagem
|
||||
path: String,
|
||||
/// Varredura rápida: só metadados dos sistemas de arquivos
|
||||
#[arg(long)]
|
||||
quick: bool,
|
||||
/// Também varre áreas ainda alocadas (mais lento)
|
||||
#[arg(long)]
|
||||
include_allocated: bool,
|
||||
/// Recupera tudo que casar com os filtros para esta pasta ao terminar
|
||||
#[arg(long)]
|
||||
recover: Option<String>,
|
||||
#[command(flatten)]
|
||||
filter: FilterArgs,
|
||||
#[command(flatten)]
|
||||
out: OutArgs,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
if let Err(e) = run(cli) {
|
||||
eprintln!("{} {e}", paint("31", "erro:"));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<(), String> {
|
||||
let eng = Engine::new();
|
||||
match cli.cmd {
|
||||
Cmd::Devices { json } => {
|
||||
let v = eng.rpc("devices", json!({})).map_err(s)?;
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
||||
return Ok(());
|
||||
}
|
||||
let list = v.as_array().cloned().unwrap_or_default();
|
||||
if list.is_empty() {
|
||||
println!("Nenhum dispositivo encontrado (ou sem permissão de leitura).");
|
||||
}
|
||||
for d in list {
|
||||
let trim = match d["trim"].as_bool() {
|
||||
Some(true) => " [TRIM ativo: apagados tendem a sumir]",
|
||||
_ => "",
|
||||
};
|
||||
println!(
|
||||
"{:<22} {:<6} {:>10} {}{}{}",
|
||||
d["path"].as_str().unwrap_or(""),
|
||||
d["kind"].as_str().unwrap_or(""),
|
||||
human(d["size"].as_u64().unwrap_or(0)),
|
||||
d["name"].as_str().unwrap_or(""),
|
||||
if d["readable"].as_bool() == Some(false) {
|
||||
" (precisa de administrador)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
trim
|
||||
);
|
||||
for m in d["mounted_at"].as_array().into_iter().flatten() {
|
||||
println!("{:<22} montado em {}", "", m.as_str().unwrap_or(""));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Inspect { path, json } => {
|
||||
let v = eng.rpc("inspect", json!({ "path": path })).map_err(s)?;
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
||||
} else {
|
||||
print_layout(&v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Scan(a) => {
|
||||
let opts = ScanOptions {
|
||||
mode: if a.quick {
|
||||
ScanMode::Quick
|
||||
} else {
|
||||
ScanMode::Deep
|
||||
},
|
||||
include_allocated: a.include_allocated,
|
||||
categories: parse_cats(&a.filter.category)?,
|
||||
..Default::default()
|
||||
};
|
||||
let job = eng.start_scan(&a.path, opts).map_err(s)?;
|
||||
drive_scan(&eng, &job, &a.out, &a.filter, a.recover.as_deref())
|
||||
}
|
||||
Cmd::Resume { session, out } => {
|
||||
let job = eng.load_session(&session).map_err(s)?;
|
||||
eng.resume(&job).map_err(s)?;
|
||||
let out = OutArgs {
|
||||
session: out.session.or(Some(session)),
|
||||
..out
|
||||
};
|
||||
drive_scan(
|
||||
&eng,
|
||||
&job,
|
||||
&out,
|
||||
&FilterArgs {
|
||||
category: vec![],
|
||||
min_confidence: "low".into(),
|
||||
name: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
Cmd::Recover {
|
||||
session,
|
||||
dest,
|
||||
filter,
|
||||
} => {
|
||||
let job = eng.load_session(&session).map_err(s)?;
|
||||
let st = eng.status(&job).map_err(s)?;
|
||||
if st["status"] == "paused" {
|
||||
return Err(
|
||||
"a varredura dessa sessão não terminou; use `guara resume` primeiro".into(),
|
||||
);
|
||||
}
|
||||
recover_job(&eng, &job, &dest, &filter)
|
||||
}
|
||||
Cmd::Image { path, dest } => {
|
||||
let job = eng.start_image(&path, &dest).map_err(s)?;
|
||||
let st = wait(&eng, &job, None)?;
|
||||
if st["status"] == "error" {
|
||||
return Err(st["error"].as_str().unwrap_or("falha").to_string());
|
||||
}
|
||||
let r = &st["report"];
|
||||
eprintln!();
|
||||
println!("Imagem criada: {dest}");
|
||||
println!("SHA-256: {}", r["sha256"].as_str().unwrap_or(""));
|
||||
let bad = r["bad_ranges"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
if bad > 0 {
|
||||
println!("Atenção: {bad} trecho(s) ilegível(is) preenchidos com zeros (veja {}.map.json).", dest.trim_end_matches(".img"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Ui { port, no_open } => serve::run(port, !no_open),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cats(v: &[String]) -> Result<Vec<Category>, String> {
|
||||
v.iter()
|
||||
.map(|c| Category::parse(c).ok_or(format!("categoria desconhecida: {c}")))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn query_from(f: &FilterArgs) -> Result<Query, String> {
|
||||
let cats = parse_cats(&f.category)?;
|
||||
let min = match f.min_confidence.as_str() {
|
||||
"low" | "baixa" => Confidence::Low,
|
||||
"medium" | "media" | "média" => Confidence::Medium,
|
||||
"high" | "alta" => Confidence::High,
|
||||
o => return Err(format!("confiança inválida: {o}")),
|
||||
};
|
||||
// Query aceita uma categoria; várias categorias são tratadas filtrando depois.
|
||||
Ok(Query {
|
||||
category: if cats.len() == 1 { Some(cats[0]) } else { None },
|
||||
min_confidence: Some(min),
|
||||
query: f.name.clone(),
|
||||
sort: Some("confidence".into()),
|
||||
offset: 0,
|
||||
limit: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
fn drive_scan(
|
||||
eng: &Engine,
|
||||
job: &str,
|
||||
out: &OutArgs,
|
||||
filter: &FilterArgs,
|
||||
recover: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let st = wait(eng, job, out.session.as_deref())?;
|
||||
eprintln!();
|
||||
match st["status"].as_str() {
|
||||
Some("error") => return Err(st["error"].as_str().unwrap_or("falha").to_string()),
|
||||
Some("paused") => {
|
||||
if let Some(p) = &out.session {
|
||||
eng.save_session(job, p).map_err(s)?;
|
||||
eprintln!("Varredura pausada. Continue com: guara resume {p}");
|
||||
} else {
|
||||
eprintln!("Varredura pausada (use --session <arquivo> para poder retomar).");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(p) = &out.session {
|
||||
eng.save_session(job, p).map_err(s)?;
|
||||
}
|
||||
if out.json {
|
||||
let res = eng.results(job, &query_from(filter)?).map_err(s)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({ "status": st, "results": res })).unwrap()
|
||||
);
|
||||
} else {
|
||||
print_summary(&st);
|
||||
if out.list {
|
||||
let q = query_from(filter)?;
|
||||
let res = eng.results(job, &Query { limit: 2000, ..q }).map_err(s)?;
|
||||
print_list(&res);
|
||||
}
|
||||
}
|
||||
if let Some(dest) = recover {
|
||||
recover_job(eng, job, dest, filter)?;
|
||||
} else if st["found"].as_u64().unwrap_or(0) > 0 && out.session.is_some() {
|
||||
eprintln!(
|
||||
"Para recuperar: guara recover {} <pasta-de-destino>",
|
||||
out.session.as_deref().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recover_job(
|
||||
eng: &Engine,
|
||||
scan_job: &str,
|
||||
dest: &str,
|
||||
filter: &FilterArgs,
|
||||
) -> Result<(), String> {
|
||||
let mut q = query_from(filter)?;
|
||||
// Várias categorias: seleciona por ids.
|
||||
let cats = parse_cats(&filter.category)?;
|
||||
let mut ids: Option<Vec<u64>> = None;
|
||||
if cats.len() > 1 {
|
||||
let mut all = vec![];
|
||||
for c in cats {
|
||||
q.category = Some(c);
|
||||
let r = eng
|
||||
.results(
|
||||
scan_job,
|
||||
&Query {
|
||||
limit: 2000,
|
||||
..q.clone()
|
||||
},
|
||||
)
|
||||
.map_err(s)?;
|
||||
all.extend(
|
||||
r["items"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|i| i["id"].as_u64()),
|
||||
);
|
||||
}
|
||||
ids = Some(all);
|
||||
}
|
||||
let rj = eng.start_recover(scan_job, ids, Some(q), dest).map_err(s)?;
|
||||
let st = wait(eng, &rj, None)?;
|
||||
eprintln!();
|
||||
if st["status"] == "error" {
|
||||
return Err(st["error"].as_str().unwrap_or("falha").to_string());
|
||||
}
|
||||
let r = &st["report"];
|
||||
let n = r["recovered"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
let failed = r["failed"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
println!(
|
||||
"{} {n} arquivo(s) recuperado(s) para {dest} ({}).",
|
||||
paint("32", "✓"),
|
||||
human(r["total_bytes"].as_u64().unwrap_or(0))
|
||||
);
|
||||
if failed > 0 {
|
||||
println!(
|
||||
"{} {failed} falharam (detalhes no manifesto).",
|
||||
paint("33", "!")
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"Manifesto com SHA-256: {}/guara-manifesto.json",
|
||||
dest.trim_end_matches('/')
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait(eng: &Engine, job: &str, session: Option<&str>) -> Result<Value, String> {
|
||||
let interrupted = Arc::new(AtomicBool::new(false));
|
||||
let flag = interrupted.clone();
|
||||
let _ = ctrlc::set_handler(move || flag.store(true, Ordering::SeqCst));
|
||||
let tty = std::io::stderr().is_terminal();
|
||||
let mut asked = false;
|
||||
loop {
|
||||
let st = eng.status(job).map_err(s)?;
|
||||
let status = st["status"].as_str().unwrap_or("").to_string();
|
||||
if interrupted.load(Ordering::SeqCst) && !asked && status == "running" {
|
||||
asked = true;
|
||||
if session.is_some() {
|
||||
eprintln!("\nPausando…");
|
||||
let _ = eng.pause(job);
|
||||
} else {
|
||||
let _ = eng.stop(job);
|
||||
}
|
||||
}
|
||||
if status != "running" {
|
||||
return Ok(st);
|
||||
}
|
||||
if tty {
|
||||
let p = &st["progress"];
|
||||
let (d, t) = (
|
||||
p["done"].as_u64().unwrap_or(0),
|
||||
p["total"].as_u64().unwrap_or(0),
|
||||
);
|
||||
let pct = d
|
||||
.checked_mul(100)
|
||||
.and_then(|v| v.checked_div(t))
|
||||
.map_or(" … ".to_string(), |v| format!("{v:>3}%"));
|
||||
let found = st["found"].as_u64().unwrap_or(0);
|
||||
let line = format!(
|
||||
"{pct} {} · {found} arquivo(s) encontrado(s) · {:.0}s",
|
||||
p["phase"].as_str().unwrap_or(""),
|
||||
st["elapsed"].as_f64().unwrap_or(0.0)
|
||||
);
|
||||
eprint!("\r\x1b[2K{line}");
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
}
|
||||
}
|
||||
|
||||
fn print_layout(v: &Value) {
|
||||
println!(
|
||||
"Tamanho: {} Tabela de partições: {}",
|
||||
human(v["size"].as_u64().unwrap_or(0)),
|
||||
v["table"].as_str().unwrap_or("-").to_uppercase()
|
||||
);
|
||||
for p in v["partitions"].as_array().into_iter().flatten() {
|
||||
let enc = p["encrypted"]
|
||||
.as_str()
|
||||
.map(|e| format!(" 🔒 cifrada ({e})"))
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
" #{:<2} início {:>12} tamanho {:>10} {:<10} {}{}{}",
|
||||
p["index"].as_u64().unwrap_or(0) + 1,
|
||||
p["start"].as_u64().unwrap_or(0),
|
||||
human(p["len"].as_u64().unwrap_or(0)),
|
||||
p["fs"].as_str().unwrap_or("?"),
|
||||
p["label"].as_str().unwrap_or(""),
|
||||
if p["lost"].as_bool() == Some(true) {
|
||||
" (perdida)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
enc
|
||||
);
|
||||
}
|
||||
for g in v["gaps"].as_array().into_iter().flatten() {
|
||||
println!(
|
||||
" espaço não particionado: início {} tamanho {}",
|
||||
g["start"],
|
||||
human(g["len"].as_u64().unwrap_or(0))
|
||||
);
|
||||
}
|
||||
for w in v["warnings"].as_array().into_iter().flatten() {
|
||||
println!("{} {}", paint("33", "!"), w.as_str().unwrap_or(""));
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summary(st: &Value) {
|
||||
println!(
|
||||
"Concluído em {:.1}s — {} arquivo(s) encontrado(s).",
|
||||
st["elapsed"].as_f64().unwrap_or(0.0),
|
||||
st["found"]
|
||||
);
|
||||
let c = &st["confidence"];
|
||||
println!(
|
||||
" confiança alta: {} média: {} baixa: {}",
|
||||
c["high"], c["medium"], c["low"]
|
||||
);
|
||||
if let Some(m) = st["categories"].as_object() {
|
||||
let mut v: Vec<_> = m.iter().collect();
|
||||
v.sort_by_key(|(k, _)| k.to_string());
|
||||
let line: Vec<String> = v.iter().map(|(k, n)| format!("{k}: {n}")).collect();
|
||||
println!(" {}", line.join(" "));
|
||||
}
|
||||
for w in st["warnings"].as_array().into_iter().flatten() {
|
||||
println!("{} {}", paint("33", "!"), w.as_str().unwrap_or(""));
|
||||
}
|
||||
}
|
||||
|
||||
fn print_list(res: &Value) {
|
||||
println!(
|
||||
"\n{:>6} {:<7} {:>10} {:<9} arquivo",
|
||||
"id", "conf.", "tamanho", "origem"
|
||||
);
|
||||
for i in res["items"].as_array().into_iter().flatten() {
|
||||
let name = match i["dir"].as_str() {
|
||||
Some(d) if !d.is_empty() => format!("{d}/{}", i["name"].as_str().unwrap_or("")),
|
||||
_ => i["name"].as_str().unwrap_or("").to_string(),
|
||||
};
|
||||
let conf = i["confidence"].as_str().unwrap_or("");
|
||||
let colored = match conf {
|
||||
"high" => paint("32", "alta "),
|
||||
"medium" => paint("33", "média "),
|
||||
_ => paint("31", "baixa "),
|
||||
};
|
||||
println!(
|
||||
"{:>6} {} {:>10} {:<9} {}",
|
||||
i["id"],
|
||||
colored,
|
||||
human(i["size"].as_u64().unwrap_or(0)),
|
||||
i["origin"].as_str().unwrap_or(""),
|
||||
name
|
||||
);
|
||||
}
|
||||
println!("({} no total)", res["total"]);
|
||||
}
|
||||
|
||||
fn human(n: u64) -> String {
|
||||
const U: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
|
||||
let mut v = n as f64;
|
||||
let mut i = 0;
|
||||
while v >= 1024.0 && i < 4 {
|
||||
v /= 1024.0;
|
||||
i += 1;
|
||||
}
|
||||
if i == 0 {
|
||||
format!("{n} B")
|
||||
} else {
|
||||
format!("{v:.1} {}", U[i])
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(code: &str, s: &str) -> String {
|
||||
if std::io::stdout().is_terminal() {
|
||||
format!("\x1b[{code}m{s}\x1b[0m")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn s<E: std::fmt::Display>(e: E) -> String {
|
||||
e.to_string()
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Servidor local da interface (127.0.0.1 + token): mesma UI do app Tauri.
|
||||
|
||||
use guara_core::service::Engine;
|
||||
use serde_json::json;
|
||||
use std::io::Read;
|
||||
use tiny_http::{Header, Method, 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 ICON: &str = include_str!("../../../apps/gui/ui/guara.svg");
|
||||
|
||||
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::getrandom(&mut tok).map_err(|e| e.to_string())?;
|
||||
let token: String = tok.iter().map(|b| format!("{b:02x}")).collect();
|
||||
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 = [
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in new issue
Block a user