mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:54:55 +08:00
feat(official-sync): 下载失败携带类型化错误码
用类型化错误(不做 message 匹配)把资源下载失败归到准确的 BAT-ERR 码:
- curl_transfer:CurlFailureKind::error_code() 映射到网络域码(403→300001、
404→300002、5xx→300005、dns/connect/timeout/tls/interrupted→30001x、
proxy→310001、其余→300015);CurlRetryError::final_error_code() 取末次失败的码。
- official_download:新增 pub DownloadError { code, message },From<String>→internal,
Display/Error;pull_* 的返回类型由 String 改为 DownloadError,内部 String 错误经
From 归 internal,下载失败边界带 pull_one 的 error_code(),非官方 URL 带
non_official_url(300030)。
- official_update:pull 错误改用 anyhow::Error::new 保留 DownloadError 类型。
- 任务 worker:downcast DownloadError 取 code 写入任务记录的 error(失败不再一律
900001),非下载失败仍归 internal。
验证:新增 kind→code 映射单测;pull 404 测试断言 DownloadError.code()==HTTP_NOT_FOUND;
lib/core 全量测试通过,clippy --all-targets 干净。
范围说明:本次覆盖资源下载路径;launcher/metadata 拉取(official_launcher)的网络
失败仍归 internal,可按同一模式后续接入。
对应 issue #1(错误码细分映射,第一批)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,44 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 官方资源下载错误:携带统一错误码,便于 CLI/RPC 归类。
|
||||
///
|
||||
/// 下载链路内部大量使用 `Result<_, String>`;这些经 `From<String>` 归入
|
||||
/// `internal`,只有 curl 下载失败等有明确来源的错误会带上准确的网络域码。
|
||||
#[derive(Debug)]
|
||||
pub struct DownloadError {
|
||||
code: bat_core::ErrorCode,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl DownloadError {
|
||||
fn new(code: bat_core::ErrorCode, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 该错误的统一错误码。
|
||||
pub fn code(&self) -> bat_core::ErrorCode {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DownloadError {
|
||||
fn from(message: String) -> Self {
|
||||
Self::new(bat_core::ErrorCode::INTERNAL, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DownloadError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DownloadError {}
|
||||
|
||||
const DOWNLOAD_MANIFEST_FILE: &str = "official-download-manifest.json";
|
||||
const DOWNLOAD_QUARANTINE_FILE: &str = "official-download-quarantine.json";
|
||||
const DOWNLOAD_MANIFEST_VERSION: u32 = 1;
|
||||
@@ -495,7 +533,7 @@ impl OfficialResourcePullService {
|
||||
pub fn pull(
|
||||
&self,
|
||||
plan: &OfficialResourcePullPlan,
|
||||
) -> Result<OfficialResourcePullReport, String> {
|
||||
) -> Result<OfficialResourcePullReport, DownloadError> {
|
||||
self.pull_with_progress(plan, |_| {})
|
||||
}
|
||||
|
||||
@@ -505,7 +543,7 @@ impl OfficialResourcePullService {
|
||||
&self,
|
||||
plan: &OfficialResourcePullPlan,
|
||||
mut progress: impl FnMut(OfficialResourcePullProgress),
|
||||
) -> Result<OfficialResourcePullReport, String> {
|
||||
) -> Result<OfficialResourcePullReport, DownloadError> {
|
||||
self.pull_with_progress_and_cancellation(plan, &mut progress, || false)
|
||||
}
|
||||
|
||||
@@ -516,7 +554,7 @@ impl OfficialResourcePullService {
|
||||
plan: &OfficialResourcePullPlan,
|
||||
mut progress: impl FnMut(OfficialResourcePullProgress),
|
||||
mut should_cancel: impl FnMut() -> bool,
|
||||
) -> Result<OfficialResourcePullReport, String> {
|
||||
) -> Result<OfficialResourcePullReport, DownloadError> {
|
||||
self.ensure_output_root_ready()?;
|
||||
|
||||
let official_hash_pairs = official_seed_hash_pairs(plan);
|
||||
@@ -530,7 +568,7 @@ impl OfficialResourcePullService {
|
||||
let mut processed_urls = HashSet::<String>::new();
|
||||
for (offset, url) in urls.into_iter().enumerate() {
|
||||
if should_cancel() {
|
||||
return Err("官方资源拉取已被停止请求中断".to_string());
|
||||
return Err("官方资源拉取已被停止请求中断".to_string().into());
|
||||
}
|
||||
|
||||
let index = offset + 1;
|
||||
@@ -541,7 +579,10 @@ impl OfficialResourcePullService {
|
||||
));
|
||||
|
||||
if !is_official_yostar_jp_url(&url) {
|
||||
return Err(format!("拒绝下载非官方 URL:{url}"));
|
||||
return Err(DownloadError::new(
|
||||
bat_core::ErrorCode::NON_OFFICIAL_URL,
|
||||
format!("拒绝下载非官方 URL:{url}"),
|
||||
));
|
||||
}
|
||||
|
||||
let destination = self.destination_for_url(&url)?;
|
||||
@@ -574,10 +615,13 @@ impl OfficialResourcePullService {
|
||||
url.clone(),
|
||||
&error,
|
||||
));
|
||||
return Err(format!(
|
||||
"官方资源下载失败:URL 已进入 quarantine,中止本轮同步、不发布不完整资源;url={url} quarantine={};{}",
|
||||
self.download_quarantine_path().display(),
|
||||
error.message
|
||||
return Err(DownloadError::new(
|
||||
error.error_code(),
|
||||
format!(
|
||||
"官方资源下载失败:URL 已进入 quarantine,中止本轮同步、不发布不完整资源;url={url} quarantine={};{}",
|
||||
self.download_quarantine_path().display(),
|
||||
error.message
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -613,7 +657,7 @@ impl OfficialResourcePullService {
|
||||
}
|
||||
|
||||
if should_cancel() {
|
||||
return Err("官方资源拉取已被停止请求中断".to_string());
|
||||
return Err("官方资源拉取已被停止请求中断".to_string().into());
|
||||
}
|
||||
self.verify_all_official_hashes_are_complete(&official_hash_pairs, &verified_hash_urls)?;
|
||||
|
||||
@@ -1552,6 +1596,14 @@ impl PullOneError {
|
||||
.and_then(CurlRetryError::final_kind_label)
|
||||
}
|
||||
|
||||
/// 映射到统一错误码:curl 重试失败取网络域码,其余(进程/续传/替换等)归 INTERNAL。
|
||||
fn error_code(&self) -> bat_core::ErrorCode {
|
||||
self.retry_error
|
||||
.as_ref()
|
||||
.and_then(CurlRetryError::final_error_code)
|
||||
.unwrap_or(bat_core::ErrorCode::INTERNAL)
|
||||
}
|
||||
|
||||
fn http_status(&self) -> Option<u16> {
|
||||
self.retry_error
|
||||
.as_ref()
|
||||
@@ -2190,7 +2242,7 @@ exit 22
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains("危险路径"));
|
||||
assert!(error.to_string().contains("危险路径"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2251,7 +2303,7 @@ exit 22
|
||||
let error = service
|
||||
.pull(&build_official_pull_plan(discovery_plan(), inventory()))
|
||||
.unwrap_err();
|
||||
assert!(error.contains("symlink"));
|
||||
assert!(error.to_string().contains("symlink"));
|
||||
assert!(fs::read_dir(&escape_dir).unwrap().next().is_none());
|
||||
}
|
||||
|
||||
@@ -2537,7 +2589,7 @@ exit 22
|
||||
};
|
||||
|
||||
let error = service.pull(&plan).unwrap_err();
|
||||
assert!(error.contains("官方 hash 校验失败"));
|
||||
assert!(error.to_string().contains("官方 hash 校验失败"));
|
||||
|
||||
let manifest = service.read_download_manifest().unwrap();
|
||||
assert!(manifest.entries.is_empty());
|
||||
@@ -2592,7 +2644,7 @@ exit 22
|
||||
|
||||
let error = service.pull(&plan).unwrap_err();
|
||||
|
||||
assert!(error.contains("ZIP"));
|
||||
assert!(error.to_string().contains("ZIP"));
|
||||
assert!(!partial_path_for(&service.destination_for_url(zip_url).unwrap()).exists());
|
||||
assert!(service.read_download_manifest().unwrap().entries.is_empty());
|
||||
}
|
||||
@@ -2801,9 +2853,11 @@ exit 22
|
||||
.pull_with_progress(&plan, |event| events.push(event))
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains("quarantine"));
|
||||
assert!(error.contains("http_not_found"));
|
||||
assert!(error.contains("retryable=false"));
|
||||
assert!(error.to_string().contains("quarantine"));
|
||||
assert!(error.to_string().contains("http_not_found"));
|
||||
assert!(error.to_string().contains("retryable=false"));
|
||||
// 类型化错误携带准确的 HTTP 404 网络域码。
|
||||
assert_eq!(error.code(), bat_core::ErrorCode::HTTP_NOT_FOUND);
|
||||
assert_eq!(
|
||||
fs::read_to_string(curl_path.with_extension("state")).unwrap(),
|
||||
"1"
|
||||
@@ -2849,8 +2903,10 @@ exit 22
|
||||
let expected_retryable = fixture["retryable"].as_bool().unwrap();
|
||||
let expected_attempts = fixture["expected_attempts"].as_u64().unwrap();
|
||||
|
||||
assert!(error.contains(expected_kind));
|
||||
assert!(error.contains(&format!("retryable={expected_retryable}")));
|
||||
assert!(error.to_string().contains(expected_kind));
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains(&format!("retryable={expected_retryable}")));
|
||||
assert_eq!(
|
||||
fs::read_to_string(curl_path.with_extension("state")).unwrap(),
|
||||
expected_attempts.to_string()
|
||||
@@ -2877,7 +2933,9 @@ exit 22
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains(fixture["expected_error_contains"].as_str().unwrap()));
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains(fixture["expected_error_contains"].as_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2895,9 +2953,9 @@ exit 22
|
||||
|
||||
let error = service.pull(&plan).unwrap_err();
|
||||
|
||||
assert!(error.contains("quarantine"));
|
||||
assert!(error.contains("http_server_error"));
|
||||
assert!(error.contains("retryable=true"));
|
||||
assert!(error.to_string().contains("quarantine"));
|
||||
assert!(error.to_string().contains("http_server_error"));
|
||||
assert!(error.to_string().contains("retryable=true"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(curl_path.with_extension("state")).unwrap(),
|
||||
"3"
|
||||
|
||||
Reference in New Issue
Block a user