fix(sync): 修复官方资源并发下载与媒体路径
bat-rust / Build and test Rust (push) Successful in 3m47s

daemon 子进程现会透传下载并发配置,下载进度按已完成数量单调上报。MediaCatalog 改为使用官方相对路径生成全局媒体 URL,覆盖 GameData、Prologue 等目录并补齐 jpg 资源,避免叶子文件名误拼媒体根目录导致 403。同步更新测试、smoke 断言和运行文档。
This commit is contained in:
2026-07-23 11:56:34 +08:00
parent d694100d6c
commit 3f5d2a8da7
16 changed files with 182 additions and 74 deletions
@@ -4471,6 +4471,8 @@ fn daemon_child_args(options: &CliOptions) -> Vec<String> {
}
args.push("--curl".to_string());
args.push(config.curl_command.to_string_lossy().to_string());
args.push("--download-concurrency".to_string());
args.push(config.download_concurrency.to_string());
match config.curl_proxy.mode() {
CurlProxyMode::Auto if options.proxy_option_explicit => {
args.push("--proxy".to_string());
@@ -6324,6 +6326,8 @@ mod tests {
"http://127.0.0.1:7890",
"--unzip",
"/usr/bin/unzip",
"--download-concurrency",
"40",
"--interval",
"30m",
"--error-retry",
@@ -6346,6 +6350,9 @@ mod tests {
assert!(args
.windows(2)
.any(|pair| pair == ["--platforms", "Windows,Android"]));
assert!(args
.windows(2)
.any(|pair| pair == ["--download-concurrency", "40"]));
// 代理凭据不得进入子进程 argv:只放不含凭据的 flag,URL 经环境变量下传。
assert!(args.contains(&PROXY_FROM_ENV_FLAG.to_string()));
assert!(!args.iter().any(|arg| arg.contains("127.0.0.1:7890")));
+28 -16
View File
@@ -118,7 +118,12 @@ impl OfficialResourcePullProgressKind {
pub struct OfficialResourcePullProgress {
/// Progress event kind.
pub kind: OfficialResourcePullProgressKind,
/// One-based URL index in the pull plan.
/// Monotonic completed item count for the whole pull.
///
/// `Started` events report the currently completed count before the URL
/// finishes; `Finished` events report the count after completion. This is
/// intentionally not the URL's plan position, because concurrent downloads
/// finish out of plan order and status percentages must not move backward.
pub index: usize,
/// Total URL count in the pull plan.
pub total: usize,
@@ -633,6 +638,7 @@ impl OfficialResourcePullService {
.collect();
let mut download_results: Vec<Option<Result<PullOneResult, PullOneError>>> =
(0..planned.len()).map(|_| None).collect();
let mut completed_count = 0usize;
if !download_indices.is_empty() {
let concurrency = self.download_concurrency.clamp(1, MAX_DOWNLOAD_CONCURRENCY);
@@ -680,7 +686,7 @@ impl OfficialResourcePullService {
WorkerMessage::Started { plan_index } => {
let item = &planned[plan_index];
progress(OfficialResourcePullProgress::started(
plan_index + 1,
completed_count,
total,
item.url.clone(),
));
@@ -711,8 +717,9 @@ impl OfficialResourcePullService {
)));
continue;
}
completed_count += 1;
progress(OfficialResourcePullProgress::finished(
plan_index + 1,
completed_count,
total,
item.url.clone(),
pull_result.status,
@@ -736,7 +743,7 @@ impl OfficialResourcePullService {
let item = &planned[plan_index];
self.record_quarantine_entry(&item.url, &item.destination, error)?;
progress(OfficialResourcePullProgress::failed(
plan_index + 1,
completed_count,
total,
item.url.clone(),
error,
@@ -768,12 +775,13 @@ impl OfficialResourcePullService {
let result = if let Some(existing) = &item.existing {
self.clear_quarantine_entry(&item.url)?;
progress(OfficialResourcePullProgress::started(
plan_index + 1,
completed_count,
total,
item.url.clone(),
));
completed_count += 1;
progress(OfficialResourcePullProgress::finished(
plan_index + 1,
completed_count,
total,
item.url.clone(),
existing.status,
@@ -2083,7 +2091,7 @@ exit 22
YostarJpDownloadInventory::from_catalog_bytes(
b"FullPatch_000.zip",
b"ExcelDB.db ExcelDB.db",
b"JP_Airi.zip",
b"GameData\\Audio\\VOC_JP\\JP_Airi.zip",
)
}
@@ -2143,10 +2151,10 @@ done
printf '%s' '{android_media_hash}' > "$out"
;;
*/MediaResources-Windows/Catalog/MediaCatalog.bytes)
printf '%s' 'JP_Airi_Win.zip' > "$out"
printf '%s' 'GameData\Audio\VOC_JP\JP_Airi_Win.zip' > "$out"
;;
*/MediaResources/Catalog/MediaCatalog.bytes)
printf '%s' 'JP_Airi_Android.zip' > "$out"
printf '%s' 'GameData\Audio\VOC_JP\JP_Airi_Android.zip' > "$out"
;;
*.zip)
printf '\120\113\003\004\024\000\000\000\000\000\000\000\000\000\000\000\000\000\002\000\000\000\002\000\000\000\010\000\000\000file.txtok\120\113\001\002\024\000\024\000\000\000\000\000\000\000\000\000\000\000\000\000\002\000\000\000\002\000\000\000\010\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000file.txt\120\113\005\006\000\000\000\000\001\000\001\000\066\000\000\000\050\000\000\000\000\000' > "$out"
@@ -2162,8 +2170,8 @@ fi
table_catalog_hash = xxhash32(b"ExcelDB.db"),
windows_bundle_hash = xxhash32(b"FullPatch_000.zip"),
android_bundle_hash = xxhash32(b"FullPatch_001.zip"),
windows_media_hash = xxhash32(b"JP_Airi_Win.zip"),
android_media_hash = xxhash32(b"JP_Airi_Android.zip"),
windows_media_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip"),
android_media_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip"),
)
}
@@ -2868,7 +2876,10 @@ exit 22
assert_eq!(xxhash32(b""), 46947589);
assert_eq!(xxhash32(b"ExcelDB.db"), 2044170421);
assert_eq!(xxhash32(b"FullPatch_000.zip"), 4038880697);
assert_eq!(xxhash32(b"JP_Airi_Win.zip"), 1416778215);
assert_eq!(
xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip"),
1865771294
);
assert_eq!(xxhash32(b"ExcelDB.db ExcelDB.db"), 2147704569);
}
@@ -3053,11 +3064,12 @@ exit 22
assert_eq!(started.len(), expected_urls);
assert_eq!(finished.len(), expected_urls);
// 每个 started 的 total 一致;index 覆盖 1..=N 且不重复
let mut indices: Vec<usize> = started.iter().map(|event| event.index).collect();
indices.sort_unstable();
assert_eq!(indices, (1..=expected_urls).collect::<Vec<_>>());
// 每个 started 的 total 一致;index 表示已完成数量,不能超过总数
assert!(started.iter().all(|event| event.index <= expected_urls));
assert!(started.iter().all(|event| event.total == expected_urls));
assert!(events.windows(2).all(|pair| pair[0].index <= pair[1].index));
let finished_indices: Vec<usize> = finished.iter().map(|event| event.index).collect();
assert_eq!(finished_indices, (1..=expected_urls).collect::<Vec<_>>());
// started 与 finished 覆盖同一组 URLfinished 均为已下载。
let started_urls: HashSet<_> = started.iter().map(|event| event.url.clone()).collect();
+5 -5
View File
@@ -186,7 +186,7 @@ mod tests {
YostarJpDownloadInventory::from_catalog_bytes(
b"FullPatch_000.zip",
b"ExcelDB.db ExcelDB.db",
b"JP_Airi.zip",
b"GameData\\Audio\\VOC_JP\\JP_Airi.zip",
)
}
@@ -197,12 +197,12 @@ mod tests {
YostarJpPlatformCatalogInventory::from_catalog_bytes(
PatchPlatform::Windows,
b"FullPatch_000.zip",
b"JP_Airi_Win.zip",
b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip",
),
YostarJpPlatformCatalogInventory::from_catalog_bytes(
PatchPlatform::Android,
b"FullPatch_001.zip",
b"JP_Airi_Android.zip",
b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip",
),
],
)
@@ -229,11 +229,11 @@ mod tests {
assert!(all_urls[0].ends_with("TableCatalog.bytes"));
assert!(all_urls
.iter()
.any(|url| url.ends_with("/MediaResources-Windows/JP_Airi.zip")));
.any(|url| url.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip")));
assert_eq!(
all_urls
.iter()
.filter(|url| url.ends_with("/MediaResources/JP_Airi.zip"))
.filter(|url| url.ends_with("/MediaResources/GameData/Audio/VOC_JP/JP_Airi.zip"))
.count(),
1
);
+3 -3
View File
@@ -1654,7 +1654,7 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new(
"download",
format!(
"下载进度:总体 {}/{} ({:.1}%);单文件开始 URL={}",
"下载进度:已完成 {}/{} ({:.1}%);单文件开始 URL={}",
event.index,
event.total,
download_progress_percent(event.index, event.total),
@@ -1667,7 +1667,7 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
OfficialUpdateProgress::new(
"download",
format!(
"下载进度:总体 {}/{} ({:.1}%);单文件完成 状态={} 文件字节={} 本轮传输字节={} URL={}",
"下载进度:已完成 {}/{} ({:.1}%);单文件完成 状态={} 文件字节={} 本轮传输字节={} URL={}",
event.index,
event.total,
download_progress_percent(event.index, event.total),
@@ -1682,7 +1682,7 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
OfficialResourcePullProgressKind::Failed => OfficialUpdateProgress::new(
"download",
format!(
"下载中断:总体 {}/{} ({:.1}%);失败类型={} HTTP={} 可重试={} 尝试次数={} quarantine={};本轮跳过该 URL 且不会发布不完整资源 URL={}",
"下载中断:已完成 {}/{} ({:.1}%);失败类型={} HTTP={} 可重试={} 尝试次数={} quarantine={};本轮跳过该 URL 且不会发布不完整资源 URL={}",
event.index,
event.total,
download_progress_percent(event.index, event.total),