Skip to content

feat(update): 支持更新包断点续传#247

Open
penguin-madagascar wants to merge 2 commits into
MistEO:mainfrom
penguin-madagascar:codex/resumable-updates
Open

feat(update): 支持更新包断点续传#247
penguin-madagascar wants to merge 2 commits into
MistEO:mainfrom
penguin-madagascar:codex/resumable-updates

Conversation

@penguin-madagascar

@penguin-madagascar penguin-madagascar commented Jun 21, 2026

Copy link
Copy Markdown

Summary

为更新包下载增加安全的断点续传能力。此前下载使用带 session ID 的临时文件,并在超时、断网等异常路径中删除半成品,用户重新下载时只能从头开始。

本 PR 将可续传更新包写入稳定的 .downloading 文件,并使用元数据校验更新包身份和服务端验证器;网络失败后再次下载或重启应用均可从已完成位置继续。用户主动取消仍会清理半成品。

Changes

  • 使用稳定半包及原子写入的元数据文件,记录 resumeKey、预期大小、SHA-256、ETag/Last-Modified 和最终文件名。
  • 续传时发送 RangeIf-Range,严格校验 206 Content-Range;服务器忽略 Range、验证器变化或返回异常 Range 时安全回退到完整下载。
  • 正确处理 416 Range Not Satisfiable,仅在本地半包大小等于远端总大小时直接完成。
  • 下载完成后校验总长度;MirrorChyan 提供 SHA-256 时同时校验哈希,避免损坏或不同版本半包进入安装流程。
  • 主动取消等待下载流和磁盘写入线程结束后再清理,避免立即切换下载源时出现并发写入。
  • 续传进度包含历史已下载字节,但实时速度只统计本次传输。
  • 清理旧版 session 临时文件,并补充中断续传、Range 回退、验证器变化、416、校验失败和取消清理测试。

Testing

  • cargo fmt --manifest-path src-tauri/Cargo.toml -- --check
  • cargo test --manifest-path src-tauri/Cargo.toml(5 tests passed)
  • cargo check --manifest-path src-tauri/Cargo.toml
  • pnpm format:check
  • pnpm build

Summary by Sourcery

添加可恢复的更新包下载功能,通过元数据验证的部分文件以及在 Tauri 后端与前端之间共享的集中式核心下载逻辑。

New Features:

  • 支持使用稳定的 .downloading 文件(通过恢复标识符进行键控)实现可恢复的更新包下载,并通过文件大小、HTTP 校验字段以及可选的 SHA-256 进行验证。
  • 通过前端将 resumeKey 和可选的 SHA-256 从更新检查结果传递到后端下载调用,以实现对部分更新包的安全复用。

Bug Fixes:

  • 通过在启动新的下载之前等待后端下载 Promise 完成,防止用户在取消并立即重新开始下载时出现并发写入和状态不一致的问题。
  • 确保更新制品清理逻辑会删除旧的基于会话的部分文件以及新的元数据/临时文件,以避免残留的过期下载文件。

Enhancements:

  • 将下载处理重构为专门的 download_core 模块,用于管理 Range/If-Range 逻辑、Content-Range 验证、部分文件兼容性检查以及原子化的元数据写入。
  • 改进进度上报:在报告中包含历史已下载字节数,同时仅基于当前传输计算吞吐量。
  • 在引入新的可恢复格式时移除旧的带会话后缀的 .downloading 文件,以保持缓存目录整洁。

Tests:

  • 为新的 download_core 模块添加类似集成测试,用于覆盖中断传输、Range 回退行为、校验字段变化、416 处理、校验和失败以及主动取消后的清理等场景。
Original summary in English

Summary by Sourcery

Add resumable update package downloading with metadata-validated partial files and centralized core download logic shared by the Tauri backend and frontend.

New Features:

  • Support resumable update package downloads using stable .downloading files keyed by a resume identifier and validated via size, HTTP validators, and optional SHA-256.
  • Expose resumeKey and optional SHA-256 from update checks through the frontend to backend download calls to enable safe reuse of partial update packages.

Bug Fixes:

  • Prevent concurrent writes and inconsistent state when users cancel and immediately restart downloads by awaiting the backend download promise before starting a new one.
  • Ensure update artifact cleanup removes legacy session-based partial files and new metadata/temporary files to avoid stale download remnants.

Enhancements:

  • Refactor download handling into a dedicated download_core module that manages Range/If-Range logic, Content-Range validation, partial file compatibility checks, and atomic metadata writes.
  • Improve progress reporting to include historical downloaded bytes while computing throughput from the current transfer only.
  • Remove legacy session-suffixed .downloading files when introducing the new resumable format to keep the cache directory clean.

Tests:

  • Add integration-style tests for interrupted transfers, Range fallback behavior, validator changes, 416 handling, checksum failures, and active cancellation cleanup in the new download_core module.
Original summary in English

Summary by Sourcery

添加可恢复的更新包下载功能,通过元数据验证的部分文件以及在 Tauri 后端与前端之间共享的集中式核心下载逻辑。

New Features:

  • 支持使用稳定的 .downloading 文件(通过恢复标识符进行键控)实现可恢复的更新包下载,并通过文件大小、HTTP 校验字段以及可选的 SHA-256 进行验证。
  • 通过前端将 resumeKey 和可选的 SHA-256 从更新检查结果传递到后端下载调用,以实现对部分更新包的安全复用。

Bug Fixes:

  • 通过在启动新的下载之前等待后端下载 Promise 完成,防止用户在取消并立即重新开始下载时出现并发写入和状态不一致的问题。
  • 确保更新制品清理逻辑会删除旧的基于会话的部分文件以及新的元数据/临时文件,以避免残留的过期下载文件。

Enhancements:

  • 将下载处理重构为专门的 download_core 模块,用于管理 Range/If-Range 逻辑、Content-Range 验证、部分文件兼容性检查以及原子化的元数据写入。
  • 改进进度上报:在报告中包含历史已下载字节数,同时仅基于当前传输计算吞吐量。
  • 在引入新的可恢复格式时移除旧的带会话后缀的 .downloading 文件,以保持缓存目录整洁。

Tests:

  • 为新的 download_core 模块添加类似集成测试,用于覆盖中断传输、Range 回退行为、校验字段变化、416 处理、校验和失败以及主动取消后的清理等场景。
Original summary in English

Summary by Sourcery

Add resumable update package downloading with metadata-validated partial files and centralized core download logic shared by the Tauri backend and frontend.

New Features:

  • Support resumable update package downloads using stable .downloading files keyed by a resume identifier and validated via size, HTTP validators, and optional SHA-256.
  • Expose resumeKey and optional SHA-256 from update checks through the frontend to backend download calls to enable safe reuse of partial update packages.

Bug Fixes:

  • Prevent concurrent writes and inconsistent state when users cancel and immediately restart downloads by awaiting the backend download promise before starting a new one.
  • Ensure update artifact cleanup removes legacy session-based partial files and new metadata/temporary files to avoid stale download remnants.

Enhancements:

  • Refactor download handling into a dedicated download_core module that manages Range/If-Range logic, Content-Range validation, partial file compatibility checks, and atomic metadata writes.
  • Improve progress reporting to include historical downloaded bytes while computing throughput from the current transfer only.
  • Remove legacy session-suffixed .downloading files when introducing the new resumable format to keep the cache directory clean.

Tests:

  • Add integration-style tests for interrupted transfers, Range fallback behavior, validator changes, 416 handling, checksum failures, and active cancellation cleanup in the new download_core module.

@penguin-madagascar penguin-madagascar marked this pull request as ready for review June 21, 2026 14:34

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src-tauri/src/commands/download_core.rs" line_range="506-248" />
<code_context>
+        .to_ascii_lowercase()
+}
+
+fn verify_sha256(path: &Path, expected: &str) -> Result<(), String> {
+    if expected.len() != 64 || !expected.chars().all(|c| c.is_ascii_hexdigit()) {
+        return Err("更新包 SHA-256 格式无效".to_string());
+    }
+    let file = File::open(path).map_err(|e| format!("无法读取下载文件: {}", e))?;
+    let mut reader = BufReader::new(file);
+    let mut hasher = Sha256::new();
+    let mut buffer = [0u8; 64 * 1024];
+    loop {
+        let read = reader
+            .read(&mut buffer)
+            .map_err(|e| format!("无法校验下载文件: {}", e))?;
+        if read == 0 {
+            break;
+        }
+        hasher.update(&buffer[..read]);
+    }
+    let actual = format!("{:x}", hasher.finalize());
+    if actual != expected {
+        return Err(format!(
+            "更新包 SHA-256 校验失败: 预期 {},实际 {}",
+            expected, actual
+        ));
+    }
+    Ok(())
+}
+
</code_context>
<issue_to_address>
**suggestion (performance):** 考虑将 SHA-256 校验卸载到阻塞线程中,以避免在处理大文件时占用异步执行器

`verify_sha256` 会在调用线程(很可能是 Tokio 的 worker 线程)中同步执行完整的缓冲读取和哈希计算。在大体积安装包上,这可能会长期占用某个异步 worker,从而为其他任务带来额外延迟。

建议将校验逻辑包装在 `tokio::task::spawn_blocking`(或等效方案)中,并对结果执行 `await`,这样可以让这段 CPU 密集型的哈希计算运行在阻塞线程池上,同时保持当前的完整性校验逻辑不变。

建议实现如下:

```rust
fn normalize_sha256(value: &str) -> String {
    value
        .trim()
        .strip_prefix("sha256:")
        .unwrap_or(value.trim())
        .to_ascii_lowercase()
}

fn verify_sha256_sync(path: &Path, expected: &str) -> Result<(), String> {
    if expected.len() != 64 || !expected.chars().all(|c| c.is_ascii_hexdigit()) {
        return Err("更新包 SHA-256 格式无效".to_string());
    }
    let file = File::open(path).map_err(|e| format!("无法读取下载文件: {}", e))?;
    let mut reader = BufReader::new(file);
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 64 * 1024];
    loop {
        let read = reader
            .read(&mut buffer)
            .map_err(|e| format!("无法校验下载文件: {}", e))?;
        if read == 0 {
            break;
        }
        hasher.update(&buffer[..read]);
    }
    let actual = format!("{:x}", hasher.finalize());
    if actual != expected {
        return Err(format!(
            "更新包 SHA-256 校验失败: 预期 {},实际 {}",
            expected, actual
        ));
    }
    Ok(())
}

async fn verify_sha256(path: &Path, expected: &str) -> Result<(), String> {
    let path = path.to_path_buf();
    let expected = expected.to_string();

    tokio::task::spawn_blocking(move || verify_sha256_sync(&path, &expected))
        .await
        .map_err(|e| format!("SHA-256 校验任务失败: {}", e))?
}

use tokio::task;
use super::update::move_to_old_folder;

```

为了完全采用非阻塞行为,你还需要:

1. 将所有调用 `verify_sha256` 的地方更新为对该异步函数执行 `await`,例如:
   - 在异步函数中,把 `verify_sha256(path, expected_sha)?;` 修改为 `verify_sha256(path, expected_sha).await?;`2. 确保所有调用 `verify_sha256` 的函数本身是 `async`(或能够执行 `.await`),并让它们的调用者相应地传播 `async`/`await`3. 如果你不希望直接暴露一个 `async fn verify_sha256`,也可以在调用点直接使用 `tokio::task::spawn_blocking`,但上述模式能够在一个地方集中管理该行为。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的评审有帮助,请考虑分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src-tauri/src/commands/download_core.rs" line_range="506-248" />
<code_context>
+        .to_ascii_lowercase()
+}
+
+fn verify_sha256(path: &Path, expected: &str) -> Result<(), String> {
+    if expected.len() != 64 || !expected.chars().all(|c| c.is_ascii_hexdigit()) {
+        return Err("更新包 SHA-256 格式无效".to_string());
+    }
+    let file = File::open(path).map_err(|e| format!("无法读取下载文件: {}", e))?;
+    let mut reader = BufReader::new(file);
+    let mut hasher = Sha256::new();
+    let mut buffer = [0u8; 64 * 1024];
+    loop {
+        let read = reader
+            .read(&mut buffer)
+            .map_err(|e| format!("无法校验下载文件: {}", e))?;
+        if read == 0 {
+            break;
+        }
+        hasher.update(&buffer[..read]);
+    }
+    let actual = format!("{:x}", hasher.finalize());
+    if actual != expected {
+        return Err(format!(
+            "更新包 SHA-256 校验失败: 预期 {},实际 {}",
+            expected, actual
+        ));
+    }
+    Ok(())
+}
+
</code_context>
<issue_to_address>
**suggestion (performance):** Consider offloading SHA-256 verification to a blocking thread to avoid tying up async executors on large files

`verify_sha256` runs a full buffered read and hash inline on the caller thread (likely a Tokio worker). On large packages this can monopolize an async worker and introduce latency for other tasks.

Consider wrapping the verification in `tokio::task::spawn_blocking` (or equivalent) and awaiting the result so the CPU-bound hashing runs on the blocking pool while preserving the current integrity checks.

Suggested implementation:

```rust
fn normalize_sha256(value: &str) -> String {
    value
        .trim()
        .strip_prefix("sha256:")
        .unwrap_or(value.trim())
        .to_ascii_lowercase()
}

fn verify_sha256_sync(path: &Path, expected: &str) -> Result<(), String> {
    if expected.len() != 64 || !expected.chars().all(|c| c.is_ascii_hexdigit()) {
        return Err("更新包 SHA-256 格式无效".to_string());
    }
    let file = File::open(path).map_err(|e| format!("无法读取下载文件: {}", e))?;
    let mut reader = BufReader::new(file);
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 64 * 1024];
    loop {
        let read = reader
            .read(&mut buffer)
            .map_err(|e| format!("无法校验下载文件: {}", e))?;
        if read == 0 {
            break;
        }
        hasher.update(&buffer[..read]);
    }
    let actual = format!("{:x}", hasher.finalize());
    if actual != expected {
        return Err(format!(
            "更新包 SHA-256 校验失败: 预期 {},实际 {}",
            expected, actual
        ));
    }
    Ok(())
}

async fn verify_sha256(path: &Path, expected: &str) -> Result<(), String> {
    let path = path.to_path_buf();
    let expected = expected.to_string();

    tokio::task::spawn_blocking(move || verify_sha256_sync(&path, &expected))
        .await
        .map_err(|e| format!("SHA-256 校验任务失败: {}", e))?
}

use tokio::task;
use super::update::move_to_old_folder;

```

To fully adopt the non-blocking behavior, you will also need to:

1. Update all call sites of `verify_sha256` to `await` the async function, e.g.:
   - Change `verify_sha256(path, expected_sha)?;` to `verify_sha256(path, expected_sha).await?;` within async functions.
2. Ensure any functions calling `verify_sha256` are `async` (or otherwise can `.await`), and that their callers propagate `async`/`await` as needed.
3. If you prefer not to expose an `async fn verify_sha256`, you can instead call `tokio::task::spawn_blocking` directly at the call sites, but the pattern above centralizes the behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src-tauri/src/commands/download_core.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant