feat: responsive pause/stop, non-blocking update with progress bar, delete saved sessions (1.5.0)
CI / Format and clippy (push) Successful in 25s
Release / Create release (push) Successful in 13s
CI / Dependency audit (push) Successful in 11s
CI / Check desktop app (Linux) (push) Successful in 1m35s
Release / CLI (linux-x86_64) (push) Successful in 2m31s
Release / CLI (linux-aarch64) (push) Successful in 1m46s
CI / Tests (Linux) (push) Successful in 4m28s
Release / Desktop app (linux-x86_64) (push) Successful in 6m9s
Release / Desktop app (windows) (push) Successful in 4m13s
Release / Desktop app (linux-aarch64) (push) Successful in 9m51s
CI / Format and clippy (push) Successful in 25s
Release / Create release (push) Successful in 13s
CI / Dependency audit (push) Successful in 11s
CI / Check desktop app (Linux) (push) Successful in 1m35s
Release / CLI (linux-x86_64) (push) Successful in 2m31s
Release / CLI (linux-aarch64) (push) Successful in 1m46s
CI / Tests (Linux) (push) Successful in 4m28s
Release / Desktop app (linux-x86_64) (push) Successful in 6m9s
Release / Desktop app (windows) (push) Successful in 4m13s
Release / Desktop app (linux-aarch64) (push) Successful in 9m51s
- carve: check cancellation inside chunks and per hit; reassembly stage is cancellable - status exposes `stopping`; UI shows "Pausing…" immediately - Tauri update commands run off the UI thread; download percentage reported - delete autosaved sessions manually (one/all) and by retention setting - chore(release): 1.5.0 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db5b972650
commit
0b9a16e8a0
20 files changed
+262
-35
No files matched your search
@@ -64,7 +64,19 @@ pub fn carve_range(
|
||||
let scan_len = got.min(CHUNK).min((end - pos) as usize);
|
||||
ev.chunk(pos, &buf[..scan_len]);
|
||||
let mut i = 0usize;
|
||||
let mut since_check = 0u32;
|
||||
while i < scan_len {
|
||||
// Pause/stop must take effect within a fraction of a second even on slow media,
|
||||
// not only at 8 MiB chunk boundaries. The chunk is reprocessed on resume (hits at
|
||||
// offsets already found are skipped by the caller).
|
||||
since_check += 1;
|
||||
if since_check >= 2048 {
|
||||
since_check = 0;
|
||||
if cancelled() {
|
||||
*next = pos;
|
||||
return CarveEnd::Cancelled;
|
||||
}
|
||||
}
|
||||
let abs = pos + i as u64;
|
||||
if abs < skip_until {
|
||||
i += SECTOR as usize;
|
||||
@@ -78,6 +90,10 @@ pub fn carve_range(
|
||||
let allowed = cfg.categories.is_empty()
|
||||
|| cfg.categories.contains(&category_for_ext(hit.ext));
|
||||
if hit.len >= MIN_LEN && allowed {
|
||||
if cancelled() {
|
||||
*next = pos;
|
||||
return CarveEnd::Cancelled;
|
||||
}
|
||||
let action = ev.hit(abs, &hit);
|
||||
match action {
|
||||
HitAction::Skip if hit.confidence != crate::types::Confidence::Low => {
|
||||
|
||||
@@ -235,7 +235,12 @@ pub fn run(
|
||||
return Outcome::Interrupted(cp);
|
||||
}
|
||||
}
|
||||
handler.rebuild_pending();
|
||||
if !handler.rebuild_pending(cancelled) {
|
||||
// Paused/stopped during reassembly: the truncated candidates were already reported,
|
||||
// and on resume (all ranges done) the scan finishes without redoing them.
|
||||
cp.next_id = next_id.load(std::sync::atomic::Ordering::Relaxed);
|
||||
return Outcome::Interrupted(cp);
|
||||
}
|
||||
Outcome::Completed
|
||||
}
|
||||
|
||||
@@ -306,9 +311,13 @@ impl Handler<'_> {
|
||||
self.sink.replace(f);
|
||||
}
|
||||
|
||||
fn rebuild_pending(&mut self) {
|
||||
/// Reassembles queued fragmented files. Returns `false` if cancelled part-way.
|
||||
fn rebuild_pending(&mut self, cancelled: &(dyn Fn() -> bool + Sync)) -> bool {
|
||||
let pend = std::mem::take(&mut self.pending);
|
||||
for p in pend {
|
||||
if cancelled() {
|
||||
return false;
|
||||
}
|
||||
let off = p.found.first_offset().unwrap_or(0);
|
||||
if p.mp4 {
|
||||
self.rebuild_mp4(p, off);
|
||||
@@ -339,6 +348,7 @@ impl Handler<'_> {
|
||||
self.sink.replace(f);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,7 @@ impl Engine {
|
||||
"error": s.error,
|
||||
"report": s.report,
|
||||
"elapsed": elapsed,
|
||||
"stopping": s.status == Status::Running && j.cancelled(),
|
||||
"source": j.source().is_some(),
|
||||
"source_path": j.src_path.lock().unwrap().clone(),
|
||||
"session_file": j.session_file.lock().unwrap().as_ref().map(|p| p.to_string_lossy().to_string()),
|
||||
@@ -1098,11 +1099,78 @@ impl Engine {
|
||||
"só sessões automáticas podem ser apagadas por aqui",
|
||||
));
|
||||
}
|
||||
let running = self.jobs.lock().unwrap().values().any(|j| {
|
||||
j.session_file.lock().unwrap().as_deref() == Some(p)
|
||||
&& j.st.lock().unwrap().status == Status::Running
|
||||
});
|
||||
if running {
|
||||
return Err(Error::msg(
|
||||
"esta varredura ainda está em andamento; pause ou pare antes de apagar a sessão",
|
||||
));
|
||||
}
|
||||
let _ = std::fs::remove_file(p.with_extension("meta.json"));
|
||||
std::fs::remove_file(p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `true` if an open job (scanning, paused, or loaded) still autosaves to this file.
|
||||
fn session_in_use(&self, p: &Path) -> bool {
|
||||
self.jobs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.any(|j| j.session_file.lock().unwrap().as_deref() == Some(p))
|
||||
}
|
||||
|
||||
/// Deletes autosaved sessions older than `days` (`None` = the configured retention;
|
||||
/// `Some(0)` = all of them). Sessions held by open jobs are always kept.
|
||||
/// Returns how many were removed.
|
||||
pub fn sessions_prune(&self, days: Option<u32>) -> usize {
|
||||
let days = match days {
|
||||
Some(d) => d,
|
||||
None => match crate::settings::load().session_retention_days {
|
||||
0 => return 0, // retention disabled: keep everything
|
||||
d => d,
|
||||
},
|
||||
};
|
||||
let dir = sessions_dir();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
let cutoff = now - days as i64 * 86_400;
|
||||
let mut removed = 0;
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else {
|
||||
return 0;
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
let path = e.path();
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if !name.ends_with(".json") || name.ends_with(".meta.json") {
|
||||
continue;
|
||||
}
|
||||
let saved_at = std::fs::read(path.with_extension("meta.json"))
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice::<Value>(&b).ok())
|
||||
.and_then(|v| v["saved_at"].as_i64())
|
||||
.or_else(|| {
|
||||
e.metadata()
|
||||
.and_then(|m| m.modified())
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs() as i64)
|
||||
})
|
||||
.unwrap_or(now);
|
||||
if saved_at <= cutoff && !self.session_in_use(&path) {
|
||||
let _ = std::fs::remove_file(path.with_extension("meta.json"));
|
||||
if std::fs::remove_file(&path).is_ok() {
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- RPC
|
||||
|
||||
pub fn rpc(&self, method: &str, p: Value) -> Result<Value> {
|
||||
@@ -1342,6 +1410,10 @@ impl Engine {
|
||||
self.session_delete(&s("path")?)?;
|
||||
json!({})
|
||||
}
|
||||
"sessions_prune" => {
|
||||
let all = p.get("all").and_then(|v| v.as_bool()) == Some(true);
|
||||
json!({ "removed": self.sessions_prune(if all { Some(0) } else { None }) })
|
||||
}
|
||||
"session_load" => json!({ "job": self.load_session(&s("path")?)? }),
|
||||
"defaults" => {
|
||||
let home = crate::owner::user_home().to_string_lossy().to_string();
|
||||
|
||||
@@ -27,6 +27,9 @@ pub struct Settings {
|
||||
/// without sending any telemetry).
|
||||
#[serde(default = "yes")]
|
||||
pub updates: bool,
|
||||
/// Autosaved sessions older than this many days are deleted on startup (`0` = keep forever).
|
||||
#[serde(default)]
|
||||
pub session_retention_days: u32,
|
||||
}
|
||||
|
||||
fn auto() -> String {
|
||||
@@ -46,6 +49,7 @@ impl Default for Settings {
|
||||
notify: true,
|
||||
maps: false,
|
||||
updates: true,
|
||||
session_retention_days: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ struct Release {
|
||||
struct Asset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
#[serde(default)]
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
@@ -52,13 +54,25 @@ fn curl_text(url: &str) -> Result<String> {
|
||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn curl_download(url: &str, dest: &Path) -> Result<()> {
|
||||
let status = Command::new("curl")
|
||||
/// Downloads `url` to `dest`, polling the file size to report `percent` (0-100) through
|
||||
/// `on_pct` while curl runs (no stderr parsing; `total` comes from the release asset size).
|
||||
fn curl_download(url: &str, dest: &Path, total: u64, on_pct: &dyn Fn(u8)) -> Result<()> {
|
||||
let mut child = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: guara-updater", "-o"])
|
||||
.arg(dest)
|
||||
.arg(url)
|
||||
.status()
|
||||
.spawn()
|
||||
.map_err(|e| Error::msg(format!("curl {url}: {e}")))?;
|
||||
let status = loop {
|
||||
if let Some(st) = child.try_wait().map_err(|e| Error::msg(e.to_string()))? {
|
||||
break st;
|
||||
}
|
||||
let got = fs::metadata(dest).map(|m| m.len()).unwrap_or(0);
|
||||
if let Some(pct) = got.saturating_mul(100).checked_div(total) {
|
||||
on_pct(pct.min(100) as u8);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
};
|
||||
if !status.success() {
|
||||
return Err(Error::msg(format!("curl {url}: download falhou")));
|
||||
}
|
||||
@@ -145,7 +159,7 @@ pub fn apply(current_exe: &PathBuf) -> Result<String> {
|
||||
let tmp_dir = std::env::temp_dir().join("guara-update");
|
||||
fs::create_dir_all(&tmp_dir)?;
|
||||
let archive_path = tmp_dir.join(&asset.name);
|
||||
curl_download(&asset.browser_download_url, &archive_path)?;
|
||||
curl_download(&asset.browser_download_url, &archive_path, 0, &|_| {})?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
@@ -350,7 +364,12 @@ where
|
||||
let download_path = tmp_dir.join(&asset.name);
|
||||
|
||||
on_progress("downloading");
|
||||
curl_download(&asset.browser_download_url, &download_path)?;
|
||||
curl_download(
|
||||
&asset.browser_download_url,
|
||||
&download_path,
|
||||
asset.size,
|
||||
&|pct| on_progress(&format!("downloading:{pct}")),
|
||||
)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
on_progress("verifying");
|
||||
|
||||
@@ -143,6 +143,7 @@ fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
|
||||
notify: false,
|
||||
maps: true,
|
||||
updates: false,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
let s = settings::load();
|
||||
|
||||
Reference in new issue
Block a user