fix: sessões abrem mesmo sem a origem; mensagens claras de erro (0.3.1)
Antes, abrir uma sessão cuja origem não estava acessível (ou cujo arquivo não podia ser lido, como em /root) falhava por inteiro com erro vago. Agora a sessão abre, permite navegar/exportar e a origem pode ser reconectada. Testes cobrem sessões gravadas pela v0.1.0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
45d535bdb7
commit
682ba8b943
10 files changed
+213
-23
No files matched your search
@@ -63,7 +63,7 @@ pub struct Job {
|
||||
opts: ScanOptions,
|
||||
cancel: AtomicBool,
|
||||
stop_requested: AtomicBool,
|
||||
src: Option<Arc<dyn Source>>,
|
||||
src: Mutex<Option<Arc<dyn Source>>>,
|
||||
st: Mutex<State>,
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ impl Job {
|
||||
opts,
|
||||
cancel: AtomicBool::new(false),
|
||||
stop_requested: AtomicBool::new(false),
|
||||
src,
|
||||
src: Mutex::new(src),
|
||||
st: Mutex::new(State {
|
||||
status: Status::Running,
|
||||
progress: Progress::default(),
|
||||
@@ -105,6 +105,9 @@ impl Job {
|
||||
}),
|
||||
})
|
||||
}
|
||||
fn source(&self) -> Option<Arc<dyn Source>> {
|
||||
self.src.lock().unwrap().clone()
|
||||
}
|
||||
fn cancelled(&self) -> bool {
|
||||
self.cancel.load(Ordering::Relaxed)
|
||||
}
|
||||
@@ -254,6 +257,7 @@ impl Engine {
|
||||
"error": s.error,
|
||||
"report": s.report,
|
||||
"elapsed": elapsed,
|
||||
"source": j.source().is_some(),
|
||||
"hits": s.hits.len(),
|
||||
"enriching": s.enrich.map(|(d, t)| json!({ "done": d, "total": t })),
|
||||
}))
|
||||
@@ -294,7 +298,7 @@ impl Engine {
|
||||
if let Some(v) = self.thumbs.lock().unwrap().get(&key) {
|
||||
return if v.is_empty() { Err(422) } else { Ok(v) };
|
||||
}
|
||||
let (f, src) = self.open_file(job, id).map_err(|_| 404u16)?;
|
||||
let (f, src) = self.open_file(job, id).map_err(status_of)?;
|
||||
if f.category != Category::Image {
|
||||
return Err(415);
|
||||
}
|
||||
@@ -319,10 +323,7 @@ impl Engine {
|
||||
.index
|
||||
.get(&file)
|
||||
.ok_or_else(|| Error::msg("arquivo não encontrado"))?;
|
||||
Ok((
|
||||
s.found[i].clone(),
|
||||
j.src.clone().ok_or_else(|| Error::msg("sem origem"))?,
|
||||
))
|
||||
Ok((s.found[i].clone(), j.source().ok_or_else(no_source)?))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- recuperação
|
||||
@@ -335,7 +336,7 @@ impl Engine {
|
||||
dest: &str,
|
||||
) -> Result<String> {
|
||||
let sj = self.job(scan_id)?;
|
||||
let src = sj.src.clone().ok_or_else(|| Error::msg("sem origem"))?;
|
||||
let src = sj.source().ok_or_else(no_source)?;
|
||||
let files: Vec<Found> = {
|
||||
let s = sj.st.lock().unwrap();
|
||||
match (&ids, &filter) {
|
||||
@@ -611,13 +612,31 @@ impl Engine {
|
||||
}
|
||||
|
||||
pub fn load_session(&self, path: &str) -> Result<String> {
|
||||
let data: SessionFile = serde_json::from_slice(&std::fs::read(path)?)?;
|
||||
let src = open_path(Path::new(&data.target)).map_err(|e| open_error(&data.target, e))?;
|
||||
let bytes = std::fs::read(path).map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
Error::msg(format!(
|
||||
"Sem permissão para ler a sessão {path}. Se ela foi criada como administrador (por exemplo em /root), abra o Guará como administrador ou copie o arquivo para a sua pasta."
|
||||
))
|
||||
} else {
|
||||
Error::msg(format!("Não foi possível ler a sessão {path}: {e}"))
|
||||
}
|
||||
})?;
|
||||
let data: SessionFile = serde_json::from_slice(&bytes).map_err(|e| {
|
||||
Error::msg(format!(
|
||||
"O arquivo {path} não é uma sessão do Guará válida (ou foi criado por uma versão incompatível): {e}"
|
||||
))
|
||||
})?;
|
||||
// A sessão abre mesmo sem a origem: dá para navegar, exportar e gerar relatório.
|
||||
// Pré-visualização e recuperação voltam quando a origem for reconectada.
|
||||
let (src, missing) = match open_path(Path::new(&data.target)) {
|
||||
Ok(s) => (Some(s), None),
|
||||
Err(e) => (None, Some(open_error(&data.target, e).to_string())),
|
||||
};
|
||||
let job = self.register(
|
||||
JobKind::Scan,
|
||||
data.target.clone(),
|
||||
data.options.clone(),
|
||||
Some(src),
|
||||
src,
|
||||
);
|
||||
{
|
||||
let mut s = job.st.lock().unwrap();
|
||||
@@ -636,12 +655,45 @@ impl Engine {
|
||||
Status::Paused
|
||||
};
|
||||
}
|
||||
if let Some(m) = missing {
|
||||
job.st.lock().unwrap().warnings.push(format!(
|
||||
"A origem {} não está disponível agora ({m}). Você pode navegar, exportar e gerar relatório; para pré-visualizar e recuperar, reconecte o disco ou escolha a origem.",
|
||||
data.target
|
||||
));
|
||||
}
|
||||
if data.done {
|
||||
spawn_enrich(job.clone());
|
||||
}
|
||||
Ok(job.id.clone())
|
||||
}
|
||||
|
||||
/// Aponta uma sessão carregada para a origem (disco ou imagem) atual.
|
||||
pub fn relink(&self, id: &str, path: &str) -> Result<()> {
|
||||
let j = self.job(id)?;
|
||||
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||
{
|
||||
let s = j.st.lock().unwrap();
|
||||
if let Some(l) = &s.layout {
|
||||
if l.size != 0 && l.size != src.size() {
|
||||
return Err(Error::msg(format!(
|
||||
"Essa origem tem {} bytes, mas a sessão foi feita sobre um disco de {} bytes. Escolha o mesmo disco ou uma imagem dele.",
|
||||
src.size(),
|
||||
l.size
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
*j.src.lock().unwrap() = Some(src);
|
||||
j.st.lock()
|
||||
.unwrap()
|
||||
.warnings
|
||||
.retain(|w| !w.starts_with("A origem "));
|
||||
if j.st.lock().unwrap().status == Status::Done {
|
||||
spawn_enrich(j);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- RPC
|
||||
|
||||
pub fn rpc(&self, method: &str, p: Value) -> Result<Value> {
|
||||
@@ -758,6 +810,10 @@ impl Engine {
|
||||
self.save_session(&s("job")?, &s("path")?)?;
|
||||
json!({})
|
||||
}
|
||||
"session_relink" => {
|
||||
self.relink(&s("job")?, &s("path")?)?;
|
||||
json!({})
|
||||
}
|
||||
"session_load" => json!({ "job": self.load_session(&s("path")?)? }),
|
||||
"defaults" => {
|
||||
let home = std::env::var("HOME")
|
||||
@@ -793,6 +849,19 @@ struct SessionFile {
|
||||
|
||||
use crate::explorer::cat_key;
|
||||
|
||||
/// 503 quando a origem da sessão está desconectada; 404 nos demais casos.
|
||||
fn status_of(e: Error) -> u16 {
|
||||
if e.to_string().contains("não está disponível") {
|
||||
503
|
||||
} else {
|
||||
404
|
||||
}
|
||||
}
|
||||
|
||||
fn no_source() -> Error {
|
||||
Error::msg("A origem desta sessão não está disponível. Reconecte o disco ou escolha a origem para pré-visualizar e recuperar.")
|
||||
}
|
||||
|
||||
fn open_error(path: &str, e: std::io::Error) -> Error {
|
||||
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
Error::msg(format!(
|
||||
@@ -805,7 +874,12 @@ fn open_error(path: &str, e: std::io::Error) -> Error {
|
||||
|
||||
fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
|
||||
std::thread::spawn(move || {
|
||||
let src = job.src.clone().expect("origem");
|
||||
let Some(src) = job.source() else {
|
||||
let mut s = job.st.lock().unwrap();
|
||||
s.status = Status::Error;
|
||||
s.error = Some(no_source().to_string());
|
||||
return;
|
||||
};
|
||||
let existing: Vec<Found> = job.st.lock().unwrap().found.clone();
|
||||
let cancelled = || job.cancelled();
|
||||
let mut sink = JobSink(job.clone());
|
||||
@@ -845,7 +919,7 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
|
||||
/// navegar por data, câmera e resolução.
|
||||
fn spawn_enrich(job: Arc<Job>) {
|
||||
std::thread::spawn(move || {
|
||||
let Some(src) = job.src.clone() else { return };
|
||||
let Some(src) = job.source() else { return };
|
||||
let todo: Vec<(u64, Found)> = {
|
||||
let s = job.st.lock().unwrap();
|
||||
s.found
|
||||
@@ -1045,7 +1119,7 @@ impl Engine {
|
||||
id: u64,
|
||||
range: Option<&str>,
|
||||
) -> std::result::Result<FileSlice, u16> {
|
||||
let (found, src) = self.open_file(job, id).map_err(|_| 404u16)?;
|
||||
let (found, src) = self.open_file(job, id).map_err(status_of)?;
|
||||
let total = found.size;
|
||||
let (mut start, mut end, mut partial) = (0u64, total.saturating_sub(1), false);
|
||||
if let Some(spec) = range.and_then(|r| r.strip_prefix("bytes=")) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version": 1, "target": "@@ALVO@@", "options": {"mode": "deep", "include_allocated": false, "categories": [], "max_carve_size": 8589934592}, "found": [{"id": 1, "name": "tese.docx", "dir": null, "ext": "docx", "category": "document", "size": 411277, "origin": "ntfs", "confidence": "high", "data": {"kind": "extents", "extents": [{"offset": 52428800, "len": 411277}]}, "modified": 1792346095, "note": null}, {"id": 2, "name": "notas.txt", "dir": null, "ext": "txt", "category": "document", "size": 945617, "origin": "ntfs", "confidence": "medium", "data": {"kind": "extents", "extents": [{"offset": 52842496, "len": 945617}]}, "modified": 1792346095, "note": null}, {"id": 3, "name": "relatorio.pdf", "dir": null, "ext": "pdf", "category": "document", "size": 646931, "origin": "ntfs", "confidence": "high", "data": {"kind": "extents", "extents": [{"offset": 53788672, "len": 646931}]}, "modified": 1792346095, "note": null}, {"id": 4, "name": "dados.bin", "dir": null, "ext": "bin", "category": "other", "size": 3000000, "origin": "ntfs", "confidence": "medium", "data": {"kind": "extents", "extents": [{"offset": 54435840, "len": 3000000}]}, "modified": 1792346095, "note": null}, {"id": 5, "name": "contatos.db", "dir": null, "ext": "db", "category": "database", "size": 573440, "origin": "ntfs", "confidence": "high", "data": {"kind": "extents", "extents": [{"offset": 57438208, "len": 573440}]}, "modified": 1792346095, "note": null}, {"id": 6, "name": "praia.jpg", "dir": null, "ext": "jpg", "category": "image", "size": 511391, "origin": "ntfs", "confidence": "high", "data": {"kind": "extents", "extents": [{"offset": 58011648, "len": 511391}]}, "modified": 1792346096, "note": null}, {"id": 7, "name": "montanha.png", "dir": null, "ext": "png", "category": "image", "size": 3337460, "origin": "ntfs", "confidence": "high", "data": {"kind": "extents", "extents": [{"offset": 58638336, "len": 3337460}]}, "modified": 1792346096, "note": null}], "checkpoint": null, "layout": {"size": 100663296, "table": "none", "partitions": [{"index": 0, "start": 0, "len": 100663296, "fs": "ntfs", "label": null, "lost": false, "encrypted": null, "table_type": "volume sem tabela de partições"}], "gaps": [], "warnings": []}, "warnings": [], "done": true}
|
||||
@@ -302,3 +302,102 @@ fn configuracoes_de_tema_persistem() {
|
||||
.unwrap();
|
||||
assert_eq!(settings::load().theme, "dark");
|
||||
}
|
||||
|
||||
fn wait_done(eng: &service::Engine, job: &str) {
|
||||
for _ in 0..500 {
|
||||
if eng.status(job).unwrap()["status"] != "running" {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
panic!("job não terminou");
|
||||
}
|
||||
|
||||
/// Uma sessão gravada pela v0.1.0 continua abrindo nas versões novas.
|
||||
#[test]
|
||||
fn sessao_da_v0_1_0_abre_e_funciona() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let img = dir.join("ntfs.img");
|
||||
let raw = include_str!("data/sessao-v0.1.0.json").replace("@@ALVO@@", img.to_str().unwrap());
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(tmp.path(), raw).unwrap();
|
||||
let eng = service::Engine::new();
|
||||
let job = eng.load_session(tmp.path().to_str().unwrap()).unwrap();
|
||||
wait_done(&eng, &job);
|
||||
let st = eng.status(&job).unwrap();
|
||||
assert_eq!(st["status"], "done");
|
||||
assert_eq!(st["found"], 7);
|
||||
assert_eq!(st["source"], true);
|
||||
let res = eng.results(&job, &service::Query::default()).unwrap();
|
||||
assert_eq!(res["total"], 7);
|
||||
// recuperar a partir da sessão antiga continua dando o mesmo conteúdo
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
let rj = eng
|
||||
.start_recover(
|
||||
&job,
|
||||
None,
|
||||
Some(service::Query::default()),
|
||||
out.path().to_str().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
wait_done(&eng, &rj);
|
||||
let got = hashes_in(out.path());
|
||||
for (h, p) in expected(&dir, "ntfs") {
|
||||
assert!(got.contains_key(&h), "faltou {p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Origem indisponível não impede abrir a sessão; reapontar a origem religa tudo.
|
||||
#[test]
|
||||
fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let img = dir.join("ntfs.img");
|
||||
let raw =
|
||||
include_str!("data/sessao-v0.1.0.json").replace("@@ALVO@@", "/dev/disco-que-nao-existe");
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(tmp.path(), raw).unwrap();
|
||||
let eng = service::Engine::new();
|
||||
let job = eng.load_session(tmp.path().to_str().unwrap()).unwrap();
|
||||
let st = eng.status(&job).unwrap();
|
||||
assert_eq!(st["source"], false);
|
||||
assert_eq!(st["found"], 7);
|
||||
assert!(st["warnings"][0]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("não está disponível"));
|
||||
// navegar e exportar funcionam
|
||||
assert_eq!(
|
||||
eng.results(&job, &service::Query::default()).unwrap()["total"],
|
||||
7
|
||||
);
|
||||
let csv = tempfile::NamedTempFile::new().unwrap();
|
||||
assert_eq!(
|
||||
eng.export_csv(&job, csv.path().to_str().unwrap(), None)
|
||||
.unwrap(),
|
||||
7
|
||||
);
|
||||
// pré-visualização e recuperação avisam com clareza
|
||||
assert_eq!(eng.thumb(&job, 1, 128).err(), Some(503));
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
let e = eng
|
||||
.start_recover(&job, None, None, out.path().to_str().unwrap())
|
||||
.unwrap_err();
|
||||
assert!(e.to_string().contains("não está disponível"), "{e}");
|
||||
// reconecta
|
||||
eng.relink(&job, img.to_str().unwrap()).unwrap();
|
||||
let st = eng.status(&job).unwrap();
|
||||
assert_eq!(st["source"], true);
|
||||
assert!(st["warnings"].as_array().unwrap().is_empty());
|
||||
let rj = eng
|
||||
.start_recover(&job, None, None, out.path().to_str().unwrap())
|
||||
.unwrap();
|
||||
wait_done(&eng, &rj);
|
||||
assert_eq!(hashes_in(out.path()).len(), 7);
|
||||
// arquivo que não é sessão
|
||||
std::fs::write(tmp.path(), b"nao e json").unwrap();
|
||||
let e = eng.load_session(tmp.path().to_str().unwrap()).unwrap_err();
|
||||
assert!(
|
||||
e.to_string().contains("não é uma sessão do Guará válida"),
|
||||
"{e}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user