chore: handle download interuptions

This commit is contained in:
zhom
2026-03-16 15:48:52 +04:00
parent 76dd0d84e8
commit 2cf9013d28
5 changed files with 112 additions and 28 deletions
+51 -4
View File
@@ -308,12 +308,40 @@ impl Downloader {
.resolve_download_url(browser_type.clone(), version, download_info)
.await?;
// Determine if we have a partial file to resume
// Check existing file size — if it matches the expected size, skip download
let mut existing_size: u64 = 0;
if let Ok(meta) = std::fs::metadata(&file_path) {
existing_size = meta.len();
}
// Do a HEAD request to get the expected file size for skip/resume decisions
let head_response = self
.client
.head(&download_url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
)
.send()
.await
.ok();
let expected_size = head_response.as_ref().and_then(|r| r.content_length());
// If existing file matches expected size, skip download entirely
if existing_size > 0 {
if let Some(expected) = expected_size {
if existing_size == expected {
log::info!(
"Archive {} already exists with correct size ({} bytes), skipping download",
file_path.display(),
existing_size
);
return Ok(file_path);
}
}
}
// Build request, add Range only if we have bytes. If the server responds with 416 (Range Not
// Satisfiable), delete the partial file and retry once without the Range header.
let response = {
@@ -683,11 +711,16 @@ impl Downloader {
// Do not remove the archive here. We keep it until verification succeeds.
}
Err(e) => {
// Do not remove the archive or extracted files. Just drop the registry entry
// so it won't be reported as downloaded.
log::error!("Extraction failed for {browser_str} {version}: {e}");
// Delete the corrupt/invalid archive so a fresh download happens next time
if download_path.exists() {
log::info!("Deleting corrupt archive: {}", download_path.display());
let _ = std::fs::remove_file(&download_path);
}
let _ = self.registry.remove_browser(&browser_str, &version);
let _ = self.registry.save();
// Remove browser-version pair from downloading set on error
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
@@ -696,6 +729,20 @@ impl Downloader {
let mut tokens = DOWNLOAD_CANCELLATION_TOKENS.lock().unwrap();
tokens.remove(&download_key);
}
// Emit error stage so the UI shows a toast
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 0.0,
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "error".to_string(),
};
let _ = events::emit("download-progress", &progress);
return Err(format!("Failed to extract browser: {e}").into());
}
}
+23 -22
View File
@@ -7,7 +7,7 @@ use crate::downloader::DownloadProgress;
use crate::events;
#[cfg(target_os = "macos")]
use std::process::Command;
use tokio::process::Command;
#[cfg(target_os = "macos")]
use std::fs::create_dir_all;
@@ -232,17 +232,8 @@ impl Extractor {
&self,
file_path: &Path,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// First check file extension for DMG files since they're common on macOS
// and can have misleading magic numbers
if let Some(ext) = file_path.extension().and_then(|ext| ext.to_str()) {
if ext.to_lowercase() == "dmg" {
return Ok("dmg".to_string());
}
if ext.to_lowercase() == "msi" {
return Ok("msi".to_string());
}
}
// Always check magic bytes first — the file extension may be wrong
// (e.g. CDN serving a ZIP with .dmg extension)
let mut file = File::open(file_path)?;
let mut buffer = [0u8; 12]; // Read first 12 bytes for magic number detection
file.read_exact(&mut buffer)?;
@@ -357,16 +348,20 @@ impl Extractor {
.args([
"attach",
"-nobrowse",
"-noverify",
"-noautoopen",
"-mountpoint",
mount_point.to_str().unwrap(),
dmg_path.to_str().unwrap(),
])
.output()?;
.stdin(std::process::Stdio::null())
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::info!("Failed to mount DMG. stdout: {stdout}, stderr: {stderr}");
log::error!("Failed to mount DMG. stdout: {stdout}, stderr: {stderr}");
// Clean up mount point before returning error
let _ = fs::remove_dir_all(&mount_point);
@@ -382,12 +377,13 @@ impl Extractor {
let app_entry = match app_result {
Ok(app_path) => app_path,
Err(e) => {
log::info!("Failed to find .app in mount point: {e}");
log::error!("Failed to find .app in mount point: {e}");
// Try to unmount before returning error
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
.output()
.await;
let _ = fs::remove_dir_all(&mount_point);
return Err("No .app found after extraction".into());
@@ -407,16 +403,18 @@ impl Extractor {
app_entry.to_str().unwrap(),
app_path.to_str().unwrap(),
])
.output()?;
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("Failed to copy app: {stderr}");
log::error!("Failed to copy app: {stderr}");
// Unmount before returning error
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
.output()
.await;
let _ = fs::remove_dir_all(&mount_point);
return Err(format!("Failed to copy app: {stderr}").into());
@@ -427,18 +425,21 @@ impl Extractor {
// Remove quarantine attributes
let _ = Command::new("xattr")
.args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()])
.output();
.output()
.await;
let _ = Command::new("xattr")
.args(["-cr", app_path.to_str().unwrap()])
.output();
.output()
.await;
log::info!("Removed quarantine attributes");
// Unmount the DMG
let output = Command::new("hdiutil")
.args(["detach", mount_point.to_str().unwrap()])
.output()?;
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
+12 -1
View File
@@ -174,6 +174,13 @@ impl GeoIPDownloader {
let mmdb_path = Self::get_mmdb_file_path()?;
// Always download to a temp file first, then atomically rename.
// This prevents corruption if the app is closed mid-download.
let temp_path = mmdb_path.with_extension("mmdb.downloading");
// Remove any leftover temp file from a previous interrupted download
let _ = fs::remove_file(&temp_path).await;
// Download the file
let response = self.client.get(&download_url).send().await?;
@@ -189,7 +196,7 @@ impl GeoIPDownloader {
let total_size = response.content_length().unwrap_or(0);
let mut downloaded: u64 = 0;
let mut file = fs::File::create(&mmdb_path).await?;
let mut file = fs::File::create(&temp_path).await?;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
@@ -237,6 +244,10 @@ impl GeoIPDownloader {
}
file.flush().await?;
drop(file);
// Atomically replace the old database with the new one
fs::rename(&temp_path, &mmdb_path).await?;
// Write download timestamp
let timestamp_path = Self::get_timestamp_path();
+9 -1
View File
@@ -293,9 +293,17 @@ async fn fetch_dynamic_proxy(
url: String,
format: String,
) -> Result<crate::browser::ProxySettings, String> {
crate::proxy_manager::PROXY_MANAGER
let settings = crate::proxy_manager::PROXY_MANAGER
.fetch_dynamic_proxy(&url, &format)
.await?;
// Validate the proxy actually works by routing through a temporary local proxy
crate::proxy_manager::PROXY_MANAGER
.check_proxy_validity("_dynamic_test", &settings)
.await
.map_err(|e| format!("Proxy resolved but connection failed: {e}"))?;
Ok(settings)
}
#[tauri::command]
+17
View File
@@ -335,6 +335,23 @@ export function useBrowserDownload() {
`download-${browserName.toLowerCase()}-${progress.version}`,
);
setDownloadProgress(null);
} else if (progress.stage === "error") {
setDownloadingBrowsers((prev) => {
const next = new Set(prev);
next.delete(progress.browser);
return next;
});
dismissToast(
`download-${browserName.toLowerCase()}-${progress.version}`,
);
setDownloadProgress(null);
showErrorToast(
`${browserName} ${progress.version}: extraction failed`,
{
description:
"The corrupt file was deleted. It will be re-downloaded on next attempt.",
},
);
} else if (progress.stage === "completed") {
setDownloadingBrowsers((prev) => {
const next = new Set(prev);