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
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:
1 parent
5e43fa6d20
commit
b4a85722a6
28 files changed
+706
-160
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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("&", "&")
|
||||
}
|
||||
|
||||
/// "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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user