feat(recover): recover a selection of folders relative to each marked folder

A marked folder a/b/Projeto is recreated as Projeto/... with everything
inside it; single marked files go to the destination root and unmarked
files inside a marked folder are left out. recover_start accepts
{selection: {folders, ids, exclude}}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5.5 committed 2026-11-03 13:31:45 -03:00
1 parent b274f0b7ef
commit ca677a8bc1
5 files changed
+410 -11

No files matched your search

+121
View File
@@ -381,6 +381,7 @@ fn sessao_da_v0_1_0_abre_e_funciona() {
&job,
None,
Some(service::Query::default()),
None,
out.path().to_str().unwrap(),
recover::RecoverLayout::Folders,
)
@@ -437,6 +438,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
&job,
None,
None,
None,
out.path().to_str().unwrap(),
recover::RecoverLayout::Folders,
)
@@ -452,6 +454,7 @@ fn sessao_abre_sem_a_origem_e_pode_ser_reconectada() {
&job,
None,
None,
None,
out.path().to_str().unwrap(),
recover::RecoverLayout::Folders,
)
@@ -712,6 +715,7 @@ fn espaco_insuficiente_no_destino_e_avisado_antes_de_copiar() {
&job,
None,
None,
None,
out.path().to_str().unwrap(),
recover::RecoverLayout::Folders,
)
@@ -719,3 +723,120 @@ fn espaco_insuficiente_no_destino_e_avisado_antes_de_copiar() {
assert!(e.to_string().contains("Espaço insuficiente"), "{e}");
assert!(recover::free_space(out.path()).unwrap_or(0) > 0);
}
/// Folder browsing on FAT32 (deleted short names lose their first character,
/// hence `_raia.jpg`): one level at a time, and recovering a marked folder
/// recreates only that folder (plus single marked files at the root, minus the
/// unmarked ones).
#[test]
fn navegacao_por_pastas_e_recuperacao_da_selecao() {
init();
let Some(dir) = fixtures() else { return };
let img = dir.join("fat32.img");
let eng = service::Engine::new();
let job = eng
.start_scan(img.to_str().unwrap(), ScanOptions::default())
.unwrap();
wait_done(&eng, &job);
let root = eng.browse(&job, "", &service::Query::default()).unwrap();
let folders: Vec<(String, u64)> = root["folders"]
.as_array()
.unwrap()
.iter()
.filter(|f| f["path"] != "@carved")
.map(|f| {
(
f["path"].as_str().unwrap().to_string(),
f["files"].as_u64().unwrap(),
)
})
.collect();
assert!(folders.contains(&("Documentos".into(), 5)), "{folders:?}");
assert!(folders.contains(&("Fotos".into(), 2)), "{folders:?}");
let docs = eng
.browse(&job, "Documentos", &service::Query::default())
.unwrap();
assert_eq!(docs["folders"][0]["path"], "Documentos/Trabalho");
assert_eq!(docs["folders"][0]["files"], 3);
let all = eng
.results(
&job,
&service::Query {
limit: 5000,
..Default::default()
},
)
.unwrap();
let id_of = |name: &str| {
all["items"]
.as_array()
.unwrap()
.iter()
.find(|i| i["name"] == name && i["origin"] == "fat")
.unwrap_or_else(|| panic!("{name} not found"))["id"]
.as_u64()
.unwrap()
};
let selection = explorer::Selection {
folders: vec!["Documentos/Trabalho".into()],
ids: vec![id_of("_raia.jpg")],
exclude: vec![id_of("tese.docx")],
};
let out = tempfile::tempdir().unwrap();
let rj = eng
.start_recover(
&job,
None,
None,
Some(selection),
out.path().to_str().unwrap(),
recover::RecoverLayout::Folders,
)
.unwrap();
wait_done(&eng, &rj);
let got = hashes_in(out.path());
let mut paths: Vec<String> = got
.values()
.map(|p| {
p.strip_prefix(out.path())
.unwrap()
.to_string_lossy()
.replace('\\', "/")
})
.collect();
paths.sort();
assert_eq!(
paths,
vec!["Trabalho/_otas.txt", "Trabalho/relatorio.pdf", "_raia.jpg"]
);
let expected = expected(&dir, "fat32");
for (hash, original) in &expected {
let wanted = [
"Documentos/Trabalho/notas.txt",
"Documentos/Trabalho/relatorio.pdf",
"Fotos/praia.jpg",
];
if wanted.contains(&original.as_str()) {
assert!(got.contains_key(hash), "content of {original} differs");
}
}
assert!(out.path().join("guara-manifesto.json").is_file());
let empty = explorer::Selection {
folders: vec!["Nada/Aqui".into()],
..Default::default()
};
let e = eng
.start_recover(
&job,
None,
None,
Some(empty),
out.path().to_str().unwrap(),
recover::RecoverLayout::Folders,
)
.unwrap_err();
assert!(e.to_string().contains("nenhum arquivo selecionado"), "{e}");
}