release: 1.1.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

This commit is contained in:
Lucas Rayan committed 2026-10-20 13:36:18 -03:00
1 parent 5e43fa6d20
commit b4a85722a6
28 files changed
+706 -160

No files matched your search

+8 -8
View File
@@ -10,9 +10,9 @@ rust-version.workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
sha1 = "0.10"
md-5 = "0.10"
sha2 = "0.11"
sha1 = "0.11"
md-5 = "0.11"
jpeg-decoder = { version = "0.3", default-features = false }
jpeg-encoder = "0.6"
image = { version = "0.25", default-features = false, features = ["png", "gif", "bmp", "webp"] }
@@ -24,11 +24,11 @@ zip = { version = "9", default-features = false, features = ["deflate"] }
snap = "1"
ruzstd = "0.9"
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
aes = "0.8"
hmac = "0.12"
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
ripemd = "0.1"
argon2 = "0.5"
aes = "0.9"
hmac = "0.13"
pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"] }
ripemd = "0.2"
argon2 = "0.6"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+3 -4
View File
@@ -9,8 +9,7 @@ pub mod filevault;
pub mod luks;
use crate::source::Source;
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
use aes::cipher::{Block, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
use aes::{Aes128, Aes192, Aes256};
use std::io;
use std::sync::Arc;
@@ -33,7 +32,7 @@ impl AesAny {
})
}
pub fn encrypt(&self, b: &mut [u8; 16]) {
let g = GenericArray::from_mut_slice(b);
let g: &mut Block<Aes128> = (&mut b[..]).try_into().expect("bloco AES de 16 bytes");
match self {
AesAny::A128(c) => c.encrypt_block(g),
AesAny::A192(c) => c.encrypt_block(g),
@@ -41,7 +40,7 @@ impl AesAny {
}
}
pub fn decrypt(&self, b: &mut [u8; 16]) {
let g = GenericArray::from_mut_slice(b);
let g: &mut Block<Aes128> = (&mut b[..]).try_into().expect("bloco AES de 16 bytes");
match self {
AesAny::A128(c) => c.decrypt_block(g),
AesAny::A192(c) => c.decrypt_block(g),
+28 -18
View File
@@ -444,11 +444,11 @@ fn exif_epoch(s: &str) -> Option<i64> {
Some(crate::fs::days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se)
}
/// "2023:07:14 18:22:05" -> "14/07/2023 18:22:05"
/// "2023:07:14 18:22:05" -> "2023-07-14T18:22:05" (a interface formata por idioma)
fn exif_date(s: &str) -> String {
let b = s.as_bytes();
if b.len() >= 19 && b[4] == b':' && b[7] == b':' {
format!("{}/{}/{} {}", &s[8..10], &s[5..7], &s[0..4], &s[11..19])
format!("{}-{}-{}T{}", &s[0..4], &s[5..7], &s[8..10], &s[11..19])
} else {
s.to_string()
}
@@ -613,17 +613,17 @@ fn pdf_text(b: &[u8]) -> String {
b.iter().map(|&c| c as char).collect()
}
/// "D:20230714182205-03'00'" -> "14/07/2023 18:22:05"
/// "D:20230714182205-03'00'" -> "2023-07-14T18:22:05" (a interface formata por idioma)
fn pdf_date(s: &str) -> String {
let t = s.trim_start_matches("D:");
let digits: String = t.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.len() >= 8 {
let g = |a: usize, b: usize| digits.get(a..b).unwrap_or("00");
return format!(
"{}/{}/{} {}:{}:{}",
g(6, 8),
g(4, 6),
"{}-{}-{}T{}:{}:{}",
g(0, 4),
g(4, 6),
g(6, 8),
g(8, 10),
g(10, 12),
g(12, 14)
@@ -733,21 +733,18 @@ fn unescape(s: &str) -> String {
.replace("&amp;", "&")
}
/// "2023-07-14T18:22:05Z" -> "14/07/2023 18:22:05 (UTC)"
/// Datas ISO 8601 ficam como estão; a interface as formata no idioma escolhido.
fn iso_date(s: &str) -> String {
let b = s.as_bytes();
if b.len() >= 19 && b[4] == b'-' && b[10] == b'T' {
let utc = if s.ends_with('Z') { " (UTC)" } else { "" };
return format!(
"{}/{}/{} {}{}",
&s[8..10],
&s[5..7],
&s[0..4],
&s[11..19],
utc
);
let mut out = s[..19].to_string();
if s.len() == 20 && s.ends_with('Z') {
out.push('Z');
}
out
} else {
s.to_string()
}
s.to_string()
}
fn ooxml(fr: &Fr, m: &mut Meta) {
@@ -890,7 +887,7 @@ fn moov(d: &[u8], m: &mut Meta) {
if created > EPOCH_1904 {
m.add(
"Gravado em (metadado)",
fmt_unix((created - EPOCH_1904) as i64),
iso_unix((created - EPOCH_1904) as i64),
);
}
if ts > 0 && dur > 0 {
@@ -919,6 +916,19 @@ fn moov(d: &[u8], m: &mut Meta) {
}
}
/// Instante em ISO 8601 UTC ("2023-07-14T18:22:05Z"), para a interface formatar.
pub fn iso_unix(t: i64) -> String {
let days = t.div_euclid(86400);
let rem = t.rem_euclid(86400);
let (y, mo, d) = civil_from_days(days);
format!(
"{y:04}-{mo:02}-{d:02}T{:02}:{:02}:{:02}Z",
rem / 3600,
rem % 3600 / 60,
rem % 60
)
}
pub fn fmt_unix(t: i64) -> String {
let days = t.div_euclid(86400);
let rem = t.rem_euclid(86400);
+52 -2
View File
@@ -3,9 +3,59 @@
use std::process::{Command, Stdio};
/// Avisos que o núcleo envia ao sistema operacional. O texto é montado no
/// idioma efetivo da interface (`settings::ui_lang`), porque notificações não
/// passam pelo dicionário do DOM.
pub enum Notice {
ScanDone { count: usize, target: String },
RecoverDone { count: usize, destination: String },
DiskLost,
}
impl Notice {
fn text(&self, lang: &str) -> (String, String) {
let en = lang == "en";
match self {
Notice::ScanDone { count, target } => (
"Guará: scan complete".into(),
if en {
format!("{count} file(s) found in {target}.")
} else {
format!("{count} arquivo(s) encontrados em {target}.")
},
),
Notice::RecoverDone { count, destination } => (
if en {
"Guará: recovery complete".into()
} else {
"Guará: recuperação concluída".into()
},
if en {
format!("{count} file(s) saved to {destination}")
} else {
format!("{count} arquivo(s) salvos em {destination}")
},
),
Notice::DiskLost => (
if en {
"Guará: disk disconnected?".into()
} else {
"Guará: disco desconectado?".into()
},
if en {
"The scan was paused without losing anything. Reconnect the disk and continue."
.into()
} else {
"A varredura foi pausada sem perder nada. Reconecte o disco e continue.".into()
},
),
}
}
}
/// Envia uma notificação sem bloquear. Falhas são ignoradas (é só um aviso).
pub fn send(title: &str, body: &str) {
let (title, body) = (title.to_string(), body.to_string());
pub fn send(notice: Notice) {
let (title, body) = notice.text(&crate::settings::ui_lang());
std::thread::spawn(move || {
if let Some(mut c) = command(&title, &body) {
if let Ok(mut child) = c
+9 -17
View File
@@ -531,14 +531,10 @@ impl Engine {
&& s.started.elapsed().as_secs() >= 8
&& crate::settings::load().notify
{
crate::notify::send(
"Guará: recuperação concluída",
&format!(
"{} arquivo(s) salvos em {}",
rep.recovered.len(),
rep.destination
),
);
crate::notify::send(crate::notify::Notice::RecoverDone {
count: rep.recovered.len(),
destination: rep.destination.clone(),
});
}
s.status = if rep.cancelled {
Status::Stopped
@@ -1300,14 +1296,10 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
s.status = Status::Done;
s.checkpoint = None;
if s.started.elapsed().as_secs() >= 20 && crate::settings::load().notify {
crate::notify::send(
"Guará: varredura concluída",
&format!(
"{} arquivo(s) encontrados em {}.",
s.found.len(),
job.target
),
);
crate::notify::send(crate::notify::Notice::ScanDone {
count: s.found.len(),
target: job.target.clone(),
});
}
}
Outcome::Interrupted(cp) => {
@@ -1317,7 +1309,7 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
s.status = Status::Paused;
if cp.io_lost {
if crate::settings::load().notify {
crate::notify::send("Guará: disco desconectado?", "A varredura foi pausada sem perder nada. Reconecte o disco e continue.");
crate::notify::send(crate::notify::Notice::DiskLost);
}
s.warnings.push("A leitura do dispositivo falhou repetidamente: ele provavelmente foi desconectado. A varredura foi pausada sem perder nada. Reconecte o disco e clique em Continuar.".into());
}
+29
View File
@@ -13,6 +13,10 @@ pub struct Settings {
/// Idioma da interface: "auto", "pt" ou "en".
#[serde(default = "auto")]
pub lang: String,
/// Idioma que a interface resolveu de fato ("pt" ou "en"), para as
/// notificações do sistema e a saída do terminal acompanharem a tela.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui_lang: Option<String>,
/// Notificação do sistema quando uma tarefa longa termina.
#[serde(default = "yes")]
pub notify: bool,
@@ -34,6 +38,7 @@ impl Default for Settings {
Self {
theme: auto(),
lang: auto(),
ui_lang: None,
notify: true,
maps: false,
}
@@ -101,6 +106,30 @@ pub fn load() -> Settings {
.unwrap_or_default()
}
/// Idioma efetivo para textos gerados fora da interface (notificações e CLI).
/// Usa o que a interface informou; sem isso, a preferência e o ambiente.
pub fn ui_lang() -> String {
let s = load();
if let Some(l) = s.ui_lang.as_deref() {
if l == "pt" || l == "en" {
return l.to_string();
}
}
if s.lang == "pt" || s.lang == "en" {
return s.lang;
}
let env = std::env::var("LC_ALL")
.or_else(|_| std::env::var("LC_MESSAGES"))
.or_else(|_| std::env::var("LANG"))
.unwrap_or_default()
.to_ascii_lowercase();
if env.starts_with("pt") {
"pt".into()
} else {
"en".into()
}
}
pub fn save(s: &Settings) -> Result<()> {
crate::owner::write_owned(&file(), serde_json::to_vec_pretty(s)?)?;
Ok(())
+8 -2
View File
@@ -139,6 +139,7 @@ fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
settings::save(&settings::Settings {
theme: "light".into(),
lang: "en".into(),
ui_lang: Some("en".into()),
notify: false,
maps: true,
})
@@ -167,7 +168,10 @@ fn notificacao_chama_o_notify_send() {
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let old = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", format!("{}:{old}", bin.path().display()));
notify::send("Guará: varredura concluída", "7 arquivos");
notify::send(notify::Notice::ScanDone {
count: 7,
target: "disco".into(),
});
for _ in 0..100 {
if marker.exists() && !std::fs::read(&marker).unwrap().is_empty() {
break;
@@ -176,8 +180,10 @@ fn notificacao_chama_o_notify_send() {
}
std::env::set_var("PATH", old);
let got = String::from_utf8(std::fs::read(&marker).unwrap()).unwrap();
assert!(got.contains("Guará: "), "{got}");
assert!(
got.contains("Guará: varredura concluída") && got.contains("7 arquivos"),
got.contains("7 arquivo(s) encontrados em disco.")
|| got.contains("7 file(s) found in disco."),
"{got}"
);
}
+4 -7
View File
@@ -151,7 +151,7 @@ fn exif_camera_data_e_gps() {
assert!(m
.fields
.iter()
.any(|(k, v)| k == "Foto tirada em" && v == "14/07/2023 18:22:05"));
.any(|(k, v)| k == "Foto tirada em" && v == "2023-07-14T18:22:05"));
}
#[test]
@@ -199,7 +199,7 @@ fn metadados_de_pdf() {
assert_eq!(get("Autor").as_deref(), Some("Maria (Perita) Silva"));
assert_eq!(get("Criado com").as_deref(), Some("LibreOffice Writer"));
assert_eq!(get("Gerado por").as_deref(), Some("PDF12"));
assert_eq!(get("Criado em").as_deref(), Some("14/07/2023 18:22:05"));
assert_eq!(get("Criado em").as_deref(), Some("2023-07-14T18:22:05"));
assert_eq!(get("Páginas (aprox.)").as_deref(), Some("12"));
}
@@ -279,10 +279,7 @@ fn metadados_de_documentos_office() {
);
assert_eq!(get("Autor").as_deref(), Some("Ana Souza"));
assert_eq!(get("Última modificação por").as_deref(), Some("Bruno Lima"));
assert_eq!(
get("Criado em").as_deref(),
Some("14/07/2023 18:22:05 (UTC)")
);
assert_eq!(get("Criado em").as_deref(), Some("2023-07-14T18:22:05Z"));
assert_eq!(get("Aplicativo").as_deref(), Some("Microsoft Office Word"));
assert_eq!(get("Empresa").as_deref(), Some("ACME"));
}
@@ -323,7 +320,7 @@ fn metadados_de_mp4() {
assert_eq!(get("Duração").as_deref(), Some("0:02:05"));
assert_eq!(
get("Gravado em (metadado)").as_deref(),
Some("14/07/2023 18:22:05 (UTC)")
Some("2023-07-14T18:22:05Z")
);
assert_eq!(get("Dimensões").as_deref(), Some("1920 × 1080 px"));
}