mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:06:44 +08:00
feat: improve bat daemon cli output
This commit is contained in:
@@ -6,7 +6,7 @@ authors.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bat-official-sync"
|
name = "bat"
|
||||||
path = "src/bin/bat_official_sync.rs"
|
path = "src/bin/bat_official_sync.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,9 @@ pub use cas::FileSystemCasRepository;
|
|||||||
pub use import::{BundleSource, ImportedResource, ResourceImportReport, ResourceImportService};
|
pub use import::{BundleSource, ImportedResource, ResourceImportReport, ResourceImportService};
|
||||||
pub use official_download::{
|
pub use official_download::{
|
||||||
OfficialLocalManifestAuditItem, OfficialLocalManifestAuditReport,
|
OfficialLocalManifestAuditItem, OfficialLocalManifestAuditReport,
|
||||||
OfficialLocalManifestAuditStatus, OfficialResourcePullItem, OfficialResourcePullProgress,
|
OfficialLocalManifestAuditStatus, OfficialLocalVerificationReport, OfficialResourcePullItem,
|
||||||
OfficialResourcePullProgressKind, OfficialResourcePullReport, OfficialResourcePullService,
|
OfficialResourcePullProgress, OfficialResourcePullProgressKind, OfficialResourcePullReport,
|
||||||
OfficialResourcePullStatus,
|
OfficialResourcePullService, OfficialResourcePullStatus,
|
||||||
};
|
};
|
||||||
pub use official_game_main_config::OfficialGameMainConfigBootstrapService;
|
pub use official_game_main_config::OfficialGameMainConfigBootstrapService;
|
||||||
pub use official_launcher::{
|
pub use official_launcher::{
|
||||||
|
|||||||
@@ -226,6 +226,19 @@ impl OfficialLocalManifestAuditStatus {
|
|||||||
pub fn is_verified(self) -> bool {
|
pub fn is_verified(self) -> bool {
|
||||||
self == Self::Verified
|
self == Self::Verified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a stable status label for CLI and JSON reports.
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Verified => "verified",
|
||||||
|
Self::MissingManifestEntry => "missing_manifest_entry",
|
||||||
|
Self::UrlMismatch => "url_mismatch",
|
||||||
|
Self::DestinationMismatch => "destination_mismatch",
|
||||||
|
Self::MissingFile => "missing_file",
|
||||||
|
Self::SizeMismatch => "size_mismatch",
|
||||||
|
Self::Blake3Mismatch => "blake3_mismatch",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local download-manifest audit result for one expected official URL.
|
/// Local download-manifest audit result for one expected official URL.
|
||||||
@@ -276,6 +289,43 @@ impl OfficialLocalManifestAuditReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full local verification report for all entries currently recorded in the
|
||||||
|
/// official download manifest.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct OfficialLocalVerificationReport {
|
||||||
|
/// Per-entry manifest audit results.
|
||||||
|
pub items: Vec<OfficialLocalManifestAuditItem>,
|
||||||
|
/// Official seed hash pairs found in the local manifest.
|
||||||
|
pub official_hash_pair_count: usize,
|
||||||
|
/// Official seed hash pairs that passed xxHash32 verification.
|
||||||
|
pub official_hash_verified_count: usize,
|
||||||
|
/// Official seed hash verification failures.
|
||||||
|
pub official_hash_errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OfficialLocalVerificationReport {
|
||||||
|
/// Returns true when every local manifest entry and every local official
|
||||||
|
/// seed hash pair passed verification.
|
||||||
|
pub fn is_clean(&self) -> bool {
|
||||||
|
self.items.iter().all(|item| item.status.is_verified())
|
||||||
|
&& self.official_hash_errors.is_empty()
|
||||||
|
&& self.official_hash_verified_count == self.official_hash_pair_count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of manifest entries that passed verification.
|
||||||
|
pub fn verified_count(&self) -> usize {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.status.is_verified())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of manifest entries that need attention.
|
||||||
|
pub fn failure_count(&self) -> usize {
|
||||||
|
self.items.len().saturating_sub(self.verified_count())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Existing local state for an official pull plan.
|
/// Existing local state for an official pull plan.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct OfficialLocalResourceState {
|
pub struct OfficialLocalResourceState {
|
||||||
@@ -362,7 +412,7 @@ impl OfficialResourcePullService {
|
|||||||
) -> Result<OfficialResourcePullReport, String> {
|
) -> Result<OfficialResourcePullReport, String> {
|
||||||
fs::create_dir_all(&self.output_root).map_err(|error| {
|
fs::create_dir_all(&self.output_root).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to create output root {}: {error}",
|
"创建资源输出目录失败 {}:{error}",
|
||||||
self.output_root.display()
|
self.output_root.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
@@ -378,7 +428,7 @@ impl OfficialResourcePullService {
|
|||||||
let mut processed_urls = HashSet::<String>::new();
|
let mut processed_urls = HashSet::<String>::new();
|
||||||
for (offset, url) in urls.into_iter().enumerate() {
|
for (offset, url) in urls.into_iter().enumerate() {
|
||||||
if should_cancel() {
|
if should_cancel() {
|
||||||
return Err("official resource pull interrupted by shutdown request".to_string());
|
return Err("官方资源拉取已被停止请求中断".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = offset + 1;
|
let index = offset + 1;
|
||||||
@@ -389,16 +439,13 @@ impl OfficialResourcePullService {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if !is_official_yostar_jp_url(&url) {
|
if !is_official_yostar_jp_url(&url) {
|
||||||
return Err(format!("Refusing to download non-official URL: {url}"));
|
return Err(format!("拒绝下载非官方 URL:{url}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let destination = self.destination_for_url(&url)?;
|
let destination = self.destination_for_url(&url)?;
|
||||||
if let Some(parent) = destination.parent() {
|
if let Some(parent) = destination.parent() {
|
||||||
fs::create_dir_all(parent).map_err(|error| {
|
fs::create_dir_all(parent).map_err(|error| {
|
||||||
format!(
|
format!("创建下载目标目录失败 {}:{error}", parent.display())
|
||||||
"Failed to create destination directory {}: {error}",
|
|
||||||
parent.display()
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +490,7 @@ impl OfficialResourcePullService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if should_cancel() {
|
if should_cancel() {
|
||||||
return Err("official resource pull interrupted by shutdown request".to_string());
|
return Err("官方资源拉取已被停止请求中断".to_string());
|
||||||
}
|
}
|
||||||
self.verify_all_official_hashes_are_complete(&official_hash_pairs, &verified_hash_urls)?;
|
self.verify_all_official_hashes_are_complete(&official_hash_pairs, &verified_hash_urls)?;
|
||||||
|
|
||||||
@@ -473,6 +520,42 @@ impl OfficialResourcePullService {
|
|||||||
Ok(OfficialLocalManifestAuditReport { items })
|
Ok(OfficialLocalManifestAuditReport { items })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies every entry currently present in the local download manifest.
|
||||||
|
///
|
||||||
|
/// This is intentionally network-free. It validates each recorded file
|
||||||
|
/// using the local manifest's size and BLAKE3 digest, then verifies every
|
||||||
|
/// local seed `.bytes`/`.hash` pair that is present using the official
|
||||||
|
/// decimal xxHash32 rule.
|
||||||
|
pub fn verify_local_download_manifest(
|
||||||
|
&self,
|
||||||
|
) -> Result<OfficialLocalVerificationReport, String> {
|
||||||
|
let manifest = self.read_download_manifest()?;
|
||||||
|
let urls = manifest.entries.keys().cloned().collect::<Vec<_>>();
|
||||||
|
let mut items = Vec::with_capacity(urls.len());
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
let destination = self.destination_for_url(&url)?;
|
||||||
|
items.push(self.audit_one(&url, destination, &manifest)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairs = local_manifest_seed_hash_pairs(&manifest);
|
||||||
|
let mut official_hash_verified_count = 0;
|
||||||
|
let mut official_hash_errors = Vec::new();
|
||||||
|
for pair in &pairs {
|
||||||
|
match self.verify_official_seed_hash_pair_from_disk(pair) {
|
||||||
|
Ok(_) => official_hash_verified_count += 1,
|
||||||
|
Err(error) => official_hash_errors.push(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OfficialLocalVerificationReport {
|
||||||
|
items,
|
||||||
|
official_hash_pair_count: pairs.len(),
|
||||||
|
official_hash_verified_count,
|
||||||
|
official_hash_errors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns whether the current output root already contains local state for
|
/// Returns whether the current output root already contains local state for
|
||||||
/// this plan. This is intentionally cheaper than a full audit and is used
|
/// this plan. This is intentionally cheaper than a full audit and is used
|
||||||
/// to choose between audit-then-repair and first-time full pull flows.
|
/// to choose between audit-then-repair and first-time full pull flows.
|
||||||
@@ -508,7 +591,7 @@ impl OfficialResourcePullService {
|
|||||||
/// `MediaCatalog.bytes`.
|
/// `MediaCatalog.bytes`.
|
||||||
pub fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
|
pub fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
|
||||||
if !is_official_yostar_jp_url(url) {
|
if !is_official_yostar_jp_url(url) {
|
||||||
return Err(format!("Refusing to fetch non-official URL: {url}"));
|
return Err(format!("拒绝拉取非官方 URL:{url}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let attempts = self.retry_attempts.max(1);
|
let attempts = self.retry_attempts.max(1);
|
||||||
@@ -517,12 +600,12 @@ impl OfficialResourcePullService {
|
|||||||
match self.fetch_bytes_once(url) {
|
match self.fetch_bytes_once(url) {
|
||||||
Ok(bytes) => return Ok(bytes),
|
Ok(bytes) => return Ok(bytes),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
last_error = Some(format!("attempt {attempt}/{attempts}: {error}"));
|
last_error = Some(format!("第 {attempt}/{attempts} 次尝试失败:{error}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(last_error.unwrap_or_else(|| format!("curl failed for {url} without an attempt")))
|
Err(last_error.unwrap_or_else(|| format!("curl 未执行就已失败:{url}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_bytes_once(&self, url: &str) -> Result<Vec<u8>, String> {
|
fn fetch_bytes_once(&self, url: &str) -> Result<Vec<u8>, String> {
|
||||||
@@ -536,14 +619,14 @@ impl OfficialResourcePullService {
|
|||||||
.output()
|
.output()
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to launch curl command {}: {error}",
|
"启动 curl 命令失败 {}:{error}",
|
||||||
self.curl_command.display()
|
self.curl_command.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
return Err(format!("curl failed for {url}: {}", stderr.trim()));
|
return Err(format!("curl 拉取失败 {url}:{}", stderr.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output.stdout)
|
Ok(output.stdout)
|
||||||
@@ -559,9 +642,7 @@ impl OfficialResourcePullService {
|
|||||||
let _ = fs::remove_file(&partial);
|
let _ = fs::remove_file(&partial);
|
||||||
self.download_one(url, &partial, false)
|
self.download_one(url, &partial, false)
|
||||||
.map_err(|retry_error| {
|
.map_err(|retry_error| {
|
||||||
format!(
|
format!("续传失败 {url}:{error};清理后重新下载也失败:{retry_error}")
|
||||||
"resume failed for {url}: {error}; clean retry also failed: {retry_error}"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -570,28 +651,20 @@ impl OfficialResourcePullService {
|
|||||||
|
|
||||||
if file_len_if_exists(destination)?.is_some() {
|
if file_len_if_exists(destination)?.is_some() {
|
||||||
fs::remove_file(destination).map_err(|error| {
|
fs::remove_file(destination).map_err(|error| {
|
||||||
format!(
|
format!("替换已有目标文件失败 {}:{error}", destination.display())
|
||||||
"Failed to replace existing destination {}: {error}",
|
|
||||||
destination.display()
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::rename(&partial, destination).map_err(|error| {
|
fs::rename(&partial, destination).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to move partial download {} -> {}: {error}",
|
"移动临时下载文件失败 {} -> {}:{error}",
|
||||||
partial.display(),
|
partial.display(),
|
||||||
destination.display()
|
destination.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let bytes = fs::metadata(destination)
|
let bytes = fs::metadata(destination)
|
||||||
.map_err(|error| {
|
.map_err(|error| format!("下载完成后目标文件缺失 {}:{error}", destination.display()))?
|
||||||
format!(
|
|
||||||
"Downloaded file missing at {}: {error}",
|
|
||||||
destination.display()
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.len();
|
.len();
|
||||||
|
|
||||||
Ok(PullOneResult {
|
Ok(PullOneResult {
|
||||||
@@ -612,17 +685,13 @@ impl OfficialResourcePullService {
|
|||||||
match self.download_one_once(url, destination, resume) {
|
match self.download_one_once(url, destination, resume) {
|
||||||
Ok(()) => return Ok(()),
|
Ok(()) => return Ok(()),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
last_error = Some(format!("attempt {attempt}/{attempts}: {error}"));
|
last_error = Some(format!("第 {attempt}/{attempts} 次尝试失败:{error}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(last_error.unwrap_or_else(|| {
|
Err(last_error
|
||||||
format!(
|
.unwrap_or_else(|| format!("curl 未执行就已失败:{url} -> {}", destination.display())))
|
||||||
"curl failed for {url} -> {} without an attempt",
|
|
||||||
destination.display()
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_one_once(&self, url: &str, destination: &Path, resume: bool) -> Result<(), String> {
|
fn download_one_once(&self, url: &str, destination: &Path, resume: bool) -> Result<(), String> {
|
||||||
@@ -641,7 +710,7 @@ impl OfficialResourcePullService {
|
|||||||
|
|
||||||
let output = command.output().map_err(|error| {
|
let output = command.output().map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to launch curl command {}: {error}",
|
"启动 curl 命令失败 {}:{error}",
|
||||||
self.curl_command.display()
|
self.curl_command.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
@@ -649,7 +718,7 @@ impl OfficialResourcePullService {
|
|||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"curl failed for {url} -> {}: {}",
|
"curl 下载失败 {url} -> {}:{}",
|
||||||
destination.display(),
|
destination.display(),
|
||||||
stderr.trim()
|
stderr.trim()
|
||||||
));
|
));
|
||||||
@@ -687,7 +756,7 @@ impl OfficialResourcePullService {
|
|||||||
self.invalidate_download_manifest_hash_pair(manifest, pair)
|
self.invalidate_download_manifest_hash_pair(manifest, pair)
|
||||||
{
|
{
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"{error}; additionally failed to invalidate local manifest entries for official hash pair: {cleanup_error}"
|
"{error};同时清理官方 hash 对应的本地 manifest 条目失败:{cleanup_error}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,13 +785,13 @@ impl OfficialResourcePullService {
|
|||||||
let hash_path = self.destination_for_url(&pair.hash_url)?;
|
let hash_path = self.destination_for_url(&pair.hash_url)?;
|
||||||
let data = fs::read(&data_path).map_err(|error| {
|
let data = fs::read(&data_path).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to read official hash data file {}: {error}",
|
"读取官方 hash 对应数据文件失败 {}:{error}",
|
||||||
data_path.display()
|
data_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let hash = fs::read(&hash_path).map_err(|error| {
|
let hash = fs::read(&hash_path).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to read official hash sidecar {}: {error}",
|
"读取官方 hash sidecar 失败 {}:{error}",
|
||||||
hash_path.display()
|
hash_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
@@ -748,7 +817,7 @@ impl OfficialResourcePullService {
|
|||||||
for pair in pairs {
|
for pair in pairs {
|
||||||
if !verified_hash_urls.contains(&pair.hash_url) {
|
if !verified_hash_urls.contains(&pair.hash_url) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"official hash pair was not verified for {} using {}",
|
"官方 hash 对未完成校验:数据={} hash={}",
|
||||||
pair.data_url, pair.hash_url
|
pair.data_url, pair.hash_url
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -766,23 +835,18 @@ impl OfficialResourcePullService {
|
|||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Failed to read download manifest {}: {error}",
|
"读取下载 manifest 失败 {}:{error}",
|
||||||
path.display()
|
path.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let manifest: OfficialDownloadManifest =
|
let manifest: OfficialDownloadManifest = serde_json::from_slice(&bytes)
|
||||||
serde_json::from_slice(&bytes).map_err(|error| {
|
.map_err(|error| format!("解析下载 manifest 失败 {}:{error}", path.display()))?;
|
||||||
format!(
|
|
||||||
"Failed to parse download manifest {}: {error}",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if manifest.version != DOWNLOAD_MANIFEST_VERSION {
|
if manifest.version != DOWNLOAD_MANIFEST_VERSION {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Unsupported download manifest version {} in {}",
|
"不支持的下载 manifest 版本 {},文件 {}",
|
||||||
manifest.version,
|
manifest.version,
|
||||||
path.display()
|
path.display()
|
||||||
));
|
));
|
||||||
@@ -795,29 +859,22 @@ impl OfficialResourcePullService {
|
|||||||
let path = self.download_manifest_path();
|
let path = self.download_manifest_path();
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent).map_err(|error| {
|
fs::create_dir_all(parent).map_err(|error| {
|
||||||
format!(
|
format!("创建下载 manifest 目录失败 {}:{error}", parent.display())
|
||||||
"Failed to create download manifest directory {}: {error}",
|
|
||||||
parent.display()
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let temporary = path.with_extension("json.tmp");
|
let temporary = path.with_extension("json.tmp");
|
||||||
let bytes = serde_json::to_vec_pretty(manifest).map_err(|error| {
|
let bytes = serde_json::to_vec_pretty(manifest)
|
||||||
format!(
|
.map_err(|error| format!("序列化下载 manifest 失败 {}:{error}", path.display()))?;
|
||||||
"Failed to serialize download manifest {}: {error}",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
fs::write(&temporary, bytes).map_err(|error| {
|
fs::write(&temporary, bytes).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to write temporary download manifest {}: {error}",
|
"写入临时下载 manifest 失败 {}:{error}",
|
||||||
temporary.display()
|
temporary.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
fs::rename(&temporary, &path).map_err(|error| {
|
fs::rename(&temporary, &path).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to move download manifest {} -> {}: {error}",
|
"移动下载 manifest 失败 {} -> {}:{error}",
|
||||||
temporary.display(),
|
temporary.display(),
|
||||||
path.display()
|
path.display()
|
||||||
)
|
)
|
||||||
@@ -871,12 +928,7 @@ impl OfficialResourcePullService {
|
|||||||
destination: &Path,
|
destination: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let bytes = fs::metadata(destination)
|
let bytes = fs::metadata(destination)
|
||||||
.map_err(|error| {
|
.map_err(|error| format!("读取已下载文件信息失败 {}:{error}", destination.display()))?
|
||||||
format!(
|
|
||||||
"Failed to inspect downloaded file {}: {error}",
|
|
||||||
destination.display()
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.len();
|
.len();
|
||||||
let digest = blake3_file_hex(destination)?;
|
let digest = blake3_file_hex(destination)?;
|
||||||
let relative_destination = self.relative_destination(destination)?;
|
let relative_destination = self.relative_destination(destination)?;
|
||||||
@@ -899,7 +951,7 @@ impl OfficialResourcePullService {
|
|||||||
.strip_prefix(&self.output_root)
|
.strip_prefix(&self.output_root)
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Destination {} is not under output root {}: {error}",
|
"目标路径 {} 不在输出目录 {} 下:{error}",
|
||||||
destination.display(),
|
destination.display(),
|
||||||
self.output_root.display()
|
self.output_root.display()
|
||||||
)
|
)
|
||||||
@@ -1013,15 +1065,15 @@ impl OfficialResourcePullService {
|
|||||||
|
|
||||||
fn destination_for_url(&self, url: &str) -> Result<PathBuf, String> {
|
fn destination_for_url(&self, url: &str) -> Result<PathBuf, String> {
|
||||||
if !is_official_yostar_jp_url(url) {
|
if !is_official_yostar_jp_url(url) {
|
||||||
return Err(format!("URL is not an official JP host: {url}"));
|
return Err(format!("URL 不是官方 JP host:{url}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let rest = url
|
let rest = url
|
||||||
.strip_prefix("https://")
|
.strip_prefix("https://")
|
||||||
.ok_or_else(|| format!("Official URL must use https: {url}"))?;
|
.ok_or_else(|| format!("官方 URL 必须使用 https:{url}"))?;
|
||||||
let (host, path) = rest
|
let (host, path) = rest
|
||||||
.split_once('/')
|
.split_once('/')
|
||||||
.ok_or_else(|| format!("Official URL has no path: {url}"))?;
|
.ok_or_else(|| format!("官方 URL 缺少路径:{url}"))?;
|
||||||
|
|
||||||
let mut destination = PathBuf::from(sanitize_segment(host));
|
let mut destination = PathBuf::from(sanitize_segment(host));
|
||||||
for segment in path.split('/') {
|
for segment in path.split('/') {
|
||||||
@@ -1029,7 +1081,7 @@ impl OfficialResourcePullService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if segment == "." || segment == ".." {
|
if segment == "." || segment == ".." {
|
||||||
return Err(format!("Official URL has unsafe path segment: {url}"));
|
return Err(format!("官方 URL 包含不安全路径片段:{url}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let segment = segment.split(['?', '#']).next().unwrap_or(segment);
|
let segment = segment.split(['?', '#']).next().unwrap_or(segment);
|
||||||
@@ -1060,7 +1112,7 @@ pub fn verify_official_seed_catalog_hash(
|
|||||||
|
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"official hash mismatch for {data_url}: expected {expected} from {hash_url}, actual {actual}"
|
"官方 hash 校验失败 {data_url}:期望 {expected}(来自 {hash_url}),实际 {actual}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,6 +1149,28 @@ fn official_seed_hash_pairs(plan: &OfficialResourcePullPlan) -> Vec<OfficialSeed
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local_manifest_seed_hash_pairs(
|
||||||
|
manifest: &OfficialDownloadManifest,
|
||||||
|
) -> Vec<OfficialSeedHashPair> {
|
||||||
|
manifest
|
||||||
|
.entries
|
||||||
|
.keys()
|
||||||
|
.filter_map(|data_url| {
|
||||||
|
let hash_url = data_url
|
||||||
|
.strip_suffix(".bytes")
|
||||||
|
.map(|prefix| format!("{prefix}.hash"))?;
|
||||||
|
if !manifest.entries.contains_key(&hash_url) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(OfficialSeedHashPair {
|
||||||
|
data_url: data_url.clone(),
|
||||||
|
hash_url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn official_hash_refresh_urls(pairs: &[OfficialSeedHashPair]) -> HashSet<String> {
|
fn official_hash_refresh_urls(pairs: &[OfficialSeedHashPair]) -> HashSet<String> {
|
||||||
let mut urls = HashSet::new();
|
let mut urls = HashSet::new();
|
||||||
for pair in pairs {
|
for pair in pairs {
|
||||||
@@ -1166,12 +1240,9 @@ struct PullOneResult {
|
|||||||
fn file_len_if_exists(path: &Path) -> Result<Option<u64>, String> {
|
fn file_len_if_exists(path: &Path) -> Result<Option<u64>, String> {
|
||||||
match fs::metadata(path) {
|
match fs::metadata(path) {
|
||||||
Ok(metadata) if metadata.is_file() => Ok(Some(metadata.len())),
|
Ok(metadata) if metadata.is_file() => Ok(Some(metadata.len())),
|
||||||
Ok(_) => Err(format!(
|
Ok(_) => Err(format!("期望文件路径,但实际不是文件:{}", path.display())),
|
||||||
"Expected file path but found non-file: {}",
|
|
||||||
path.display()
|
|
||||||
)),
|
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
Err(error) => Err(format!("Failed to inspect {}: {error}", path.display())),
|
Err(error) => Err(format!("读取路径信息失败 {}:{error}", path.display())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1183,14 +1254,14 @@ fn partial_path_for(destination: &Path) -> PathBuf {
|
|||||||
|
|
||||||
fn blake3_file_hex(path: &Path) -> Result<String, String> {
|
fn blake3_file_hex(path: &Path) -> Result<String, String> {
|
||||||
let mut file =
|
let mut file =
|
||||||
File::open(path).map_err(|error| format!("Failed to open {}: {error}", path.display()))?;
|
File::open(path).map_err(|error| format!("打开文件失败 {}:{error}", path.display()))?;
|
||||||
let mut hasher = blake3::Hasher::new();
|
let mut hasher = blake3::Hasher::new();
|
||||||
let mut buffer = [0u8; 64 * 1024];
|
let mut buffer = [0u8; 64 * 1024];
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let read = file
|
let read = file
|
||||||
.read(&mut buffer)
|
.read(&mut buffer)
|
||||||
.map_err(|error| format!("Failed to read {}: {error}", path.display()))?;
|
.map_err(|error| format!("读取文件失败 {}:{error}", path.display()))?;
|
||||||
if read == 0 {
|
if read == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1202,11 +1273,10 @@ fn blake3_file_hex(path: &Path) -> Result<String, String> {
|
|||||||
|
|
||||||
fn parse_official_xxhash32_decimal(hash_url: &str, bytes: &[u8]) -> Result<u32, String> {
|
fn parse_official_xxhash32_decimal(hash_url: &str, bytes: &[u8]) -> Result<u32, String> {
|
||||||
let text = std::str::from_utf8(bytes)
|
let text = std::str::from_utf8(bytes)
|
||||||
.map_err(|error| format!("Official hash sidecar is not UTF-8 at {hash_url}: {error}"))?
|
.map_err(|error| format!("官方 hash sidecar 不是 UTF-8:{hash_url}:{error}"))?
|
||||||
.trim();
|
.trim();
|
||||||
text.parse::<u32>().map_err(|error| {
|
text.parse::<u32>()
|
||||||
format!("Official hash sidecar is not decimal xxHash32 at {hash_url}: {error}")
|
.map_err(|error| format!("官方 hash sidecar 不是十进制 xxHash32:{hash_url}:{error}"))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xxhash32(bytes: &[u8]) -> u32 {
|
fn xxhash32(bytes: &[u8]) -> u32 {
|
||||||
@@ -1649,6 +1719,37 @@ fi
|
|||||||
assert!(service.audit_local_manifest(&plan).unwrap().is_clean());
|
assert!(service.audit_local_manifest(&plan).unwrap().is_clean());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_local_verification_checks_manifest_and_official_hash_pairs() {
|
||||||
|
let out_dir = TempDir::new().unwrap();
|
||||||
|
let bin_dir = TempDir::new().unwrap();
|
||||||
|
let curl_path = bin_dir.path().join("curl");
|
||||||
|
write_fake_curl(&curl_path);
|
||||||
|
|
||||||
|
let service = OfficialResourcePullService::with_curl_command(out_dir.path(), &curl_path);
|
||||||
|
let plan = build_official_pull_plan(discovery_plan(), inventory());
|
||||||
|
service.pull(&plan).unwrap();
|
||||||
|
|
||||||
|
let verification = service.verify_local_download_manifest().unwrap();
|
||||||
|
assert!(verification.is_clean());
|
||||||
|
assert_eq!(verification.items.len(), 7);
|
||||||
|
assert_eq!(verification.verified_count(), 7);
|
||||||
|
assert_eq!(verification.official_hash_pair_count, 1);
|
||||||
|
assert_eq!(verification.official_hash_verified_count, 1);
|
||||||
|
|
||||||
|
let data_path = service
|
||||||
|
.destination_for_url(
|
||||||
|
"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(data_path, b"corrupted").unwrap();
|
||||||
|
|
||||||
|
let corrupted = service.verify_local_download_manifest().unwrap();
|
||||||
|
assert!(!corrupted.is_clean());
|
||||||
|
assert_eq!(corrupted.failure_count(), 1);
|
||||||
|
assert_eq!(corrupted.official_hash_errors.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refreshes_official_hash_pairs_and_skips_manifest_verified_content() {
|
fn refreshes_official_hash_pairs_and_skips_manifest_verified_content() {
|
||||||
let out_dir = TempDir::new().unwrap();
|
let out_dir = TempDir::new().unwrap();
|
||||||
@@ -1833,7 +1934,7 @@ fi
|
|||||||
};
|
};
|
||||||
|
|
||||||
let error = service.pull(&plan).unwrap_err();
|
let error = service.pull(&plan).unwrap_err();
|
||||||
assert!(error.contains("official hash mismatch"));
|
assert!(error.contains("官方 hash 校验失败"));
|
||||||
|
|
||||||
let manifest = service.read_download_manifest().unwrap();
|
let manifest = service.read_download_manifest().unwrap();
|
||||||
assert!(manifest.entries.is_empty());
|
assert!(manifest.entries.is_empty());
|
||||||
|
|||||||
@@ -83,10 +83,9 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
.source
|
.source
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or_else(|| "Official launcher remote manifest has no source".to_string())?;
|
.ok_or_else(|| "官方启动器远端 manifest 缺少 source".to_string())?;
|
||||||
let cdn_config = self.launcher.fetch_cdn_config()?;
|
let cdn_config = self.launcher.fetch_cdn_config()?;
|
||||||
let temp_dir = TempDir::new()
|
let temp_dir = TempDir::new().map_err(|error| format!("创建临时目录失败:{error}"))?;
|
||||||
.map_err(|error| format!("Failed to create temporary directory: {error}"))?;
|
|
||||||
let (game_zip_url, resources_assets) = self.fetch_resources_assets(
|
let (game_zip_url, resources_assets) = self.fetch_resources_assets(
|
||||||
&game_config,
|
&game_config,
|
||||||
&manifest,
|
&manifest,
|
||||||
@@ -121,12 +120,12 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
match self.download_file_once(url, destination) {
|
match self.download_file_once(url, destination) {
|
||||||
Ok(()) => return Ok(()),
|
Ok(()) => return Ok(()),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
last_error = Some(format!("attempt {attempt}/{attempts}: {error}"));
|
last_error = Some(format!("第 {attempt}/{attempts} 次尝试失败:{error}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(last_error.unwrap_or_else(|| format!("curl failed for {url} without an attempt")))
|
Err(last_error.unwrap_or_else(|| format!("curl 未执行就已失败:{url}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_file_once(&self, url: &str, destination: &Path) -> Result<(), String> {
|
fn download_file_once(&self, url: &str, destination: &Path) -> Result<(), String> {
|
||||||
@@ -142,14 +141,14 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
.output()
|
.output()
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to launch curl command {}: {error}",
|
"启动 curl 命令失败 {}:{error}",
|
||||||
self.curl_command.display()
|
self.curl_command.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
return Err(format!("curl failed for {url}: {}", stderr.trim()));
|
return Err(format!("curl 拉取失败 {url}:{}", stderr.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -168,7 +167,7 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
let backup_url = launcher_package_url(backup_cdn_root, relative_path)?;
|
let backup_url = launcher_package_url(backup_cdn_root, relative_path)?;
|
||||||
self.download_file(&backup_url, destination).map_err(|backup_error| {
|
self.download_file(&backup_url, destination).map_err(|backup_error| {
|
||||||
format!(
|
format!(
|
||||||
"failed to download official game ZIP from primary ({primary_error}) and backup ({backup_error})"
|
"下载官方游戏资源失败:主地址失败({primary_error}),备用地址也失败({backup_error})"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -195,11 +194,10 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
)?;
|
)?;
|
||||||
let extract_root = temp_root.join("extract");
|
let extract_root = temp_root.join("extract");
|
||||||
fs::create_dir_all(&extract_root)
|
fs::create_dir_all(&extract_root)
|
||||||
.map_err(|error| format!("Failed to create extract root: {error}"))?;
|
.map_err(|error| format!("创建解压目录失败:{error}"))?;
|
||||||
self.extract_archive(&archive_path, &extract_root)?;
|
self.extract_archive(&archive_path, &extract_root)?;
|
||||||
let resources_assets = find_resources_assets(&extract_root).ok_or_else(|| {
|
let resources_assets = find_resources_assets(&extract_root)
|
||||||
"resources.assets not found in official launcher package".to_string()
|
.ok_or_else(|| "官方启动器包内没有找到 resources.assets".to_string())?;
|
||||||
})?;
|
|
||||||
Ok((game_zip_url, resources_assets))
|
Ok((game_zip_url, resources_assets))
|
||||||
}
|
}
|
||||||
GameMainConfigSource::ManifestFile { source_dir, file } => {
|
GameMainConfigSource::ManifestFile { source_dir, file } => {
|
||||||
@@ -228,7 +226,7 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
.output()
|
.output()
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to launch unzip command {}: {error}",
|
"启动 unzip 命令失败 {}:{error}",
|
||||||
self.unzip_command.display()
|
self.unzip_command.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
@@ -236,7 +234,7 @@ impl OfficialGameMainConfigBootstrapService {
|
|||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"unzip failed for {}: {}",
|
"解压失败 {}:{}",
|
||||||
archive_path.display(),
|
archive_path.display(),
|
||||||
stderr.trim()
|
stderr.trim()
|
||||||
));
|
));
|
||||||
@@ -271,7 +269,7 @@ fn select_game_main_config_source<'a>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Official launcher manifest directory source has no BlueArchive_Data/resources.assets entry: {manifest_source}"
|
"官方启动器 manifest 目录 source 缺少 BlueArchive_Data/resources.assets 条目:{manifest_source}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +280,7 @@ fn select_game_main_config_source<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Official launcher metadata has no usable game archive path: source={manifest_source}, game_latest_file_path={}",
|
"官方启动器元数据没有可用的游戏包路径:source={manifest_source}, game_latest_file_path={}",
|
||||||
game_config.game_latest_file_path
|
game_config.game_latest_file_path
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -318,9 +316,9 @@ fn launcher_manifest_file_relative_path(
|
|||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let source = normalize_manifest_source_dir(source_dir)
|
let source = normalize_manifest_source_dir(source_dir)
|
||||||
.ok_or_else(|| format!("Invalid official launcher manifest source: {source_dir}"))?;
|
.ok_or_else(|| format!("官方启动器 manifest source 无效:{source_dir}"))?;
|
||||||
let file = normalize_manifest_file_path(file_path)
|
let file = normalize_manifest_file_path(file_path)
|
||||||
.ok_or_else(|| format!("Invalid official launcher manifest file path: {file_path}"))?;
|
.ok_or_else(|| format!("官方启动器 manifest 文件路径无效:{file_path}"))?;
|
||||||
Ok(format!("{source}/{file}"))
|
Ok(format!("{source}/{file}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,18 +356,16 @@ fn verify_manifest_file_size(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
file: &YostarJpLauncherManifestFile,
|
file: &YostarJpLauncherManifestFile,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let expected_size = file.size.parse::<u64>().map_err(|error| {
|
let expected_size = file
|
||||||
format!(
|
.size
|
||||||
"Official launcher manifest has invalid size for {}: {error}",
|
.parse::<u64>()
|
||||||
file.path
|
.map_err(|error| format!("官方启动器 manifest 中 {} 的 size 无效:{error}", file.path))?;
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let actual_size = fs::metadata(path)
|
let actual_size = fs::metadata(path)
|
||||||
.map_err(|error| format!("Failed to stat downloaded {}: {error}", path.display()))?
|
.map_err(|error| format!("读取已下载文件信息失败 {}:{error}", path.display()))?
|
||||||
.len();
|
.len();
|
||||||
if actual_size != expected_size {
|
if actual_size != expected_size {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Downloaded official launcher file size mismatch for {}: expected {}, got {}",
|
"已下载官方启动器文件大小不匹配 {}:期望 {},实际 {}",
|
||||||
file.path, expected_size, actual_size
|
file.path, expected_size, actual_size
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,13 +109,11 @@ impl OfficialLauncherBootstrapService {
|
|||||||
/// Fetches latest official PC game config.
|
/// Fetches latest official PC game config.
|
||||||
pub fn fetch_game_config(&self) -> Result<YostarJpLauncherGameConfig, String> {
|
pub fn fetch_game_config(&self) -> Result<YostarJpLauncherGameConfig, String> {
|
||||||
let envelope = self.fetch_launcher_envelope("/api/launcher/game/config")?;
|
let envelope = self.fetch_launcher_envelope("/api/launcher/game/config")?;
|
||||||
let config: YostarJpLauncherGameConfig =
|
let config: YostarJpLauncherGameConfig = serde_json::from_value(envelope.data)
|
||||||
serde_json::from_value(envelope.data).map_err(|error| {
|
.map_err(|error| format!("解析官方启动器 game config 数据失败:{error}"))?;
|
||||||
format!("Failed to parse official launcher game config data: {error}")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if config.game_latest_version.is_empty() || config.game_latest_file_path.is_empty() {
|
if config.game_latest_version.is_empty() || config.game_latest_file_path.is_empty() {
|
||||||
return Err("Official launcher game config is missing latest version or basis".into());
|
return Err("官方启动器 game config 缺少最新版本或基准路径".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -125,7 +123,7 @@ impl OfficialLauncherBootstrapService {
|
|||||||
pub fn fetch_cdn_config(&self) -> Result<YostarJpLauncherCdnConfig, String> {
|
pub fn fetch_cdn_config(&self) -> Result<YostarJpLauncherCdnConfig, String> {
|
||||||
let envelope = self.fetch_launcher_envelope("/api/launcher/advanced/game/download/cdn")?;
|
let envelope = self.fetch_launcher_envelope("/api/launcher/advanced/game/download/cdn")?;
|
||||||
serde_json::from_value(envelope.data)
|
serde_json::from_value(envelope.data)
|
||||||
.map_err(|error| format!("Failed to parse official launcher CDN config data: {error}"))
|
.map_err(|error| format!("解析官方启动器 CDN config 数据失败:{error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the official remote manifest URL for a PC client version and
|
/// Fetches the official remote manifest URL for a PC client version and
|
||||||
@@ -136,7 +134,7 @@ impl OfficialLauncherBootstrapService {
|
|||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> Result<YostarJpLauncherManifestUrl, String> {
|
) -> Result<YostarJpLauncherManifestUrl, String> {
|
||||||
if version.is_empty() || file_path.is_empty() {
|
if version.is_empty() || file_path.is_empty() {
|
||||||
return Err("Launcher manifest version and file path must not be empty".into());
|
return Err("启动器 manifest 版本和文件路径不能为空".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = format!(
|
let path = format!(
|
||||||
@@ -146,13 +144,11 @@ impl OfficialLauncherBootstrapService {
|
|||||||
);
|
);
|
||||||
let envelope = self.fetch_launcher_envelope(&path)?;
|
let envelope = self.fetch_launcher_envelope(&path)?;
|
||||||
let manifest_url: YostarJpLauncherManifestUrl = serde_json::from_value(envelope.data)
|
let manifest_url: YostarJpLauncherManifestUrl = serde_json::from_value(envelope.data)
|
||||||
.map_err(|error| {
|
.map_err(|error| format!("解析官方启动器 manifest URL 数据失败:{error}"))?;
|
||||||
format!("Failed to parse official launcher manifest URL data: {error}")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !is_official_launcher_package_url(&manifest_url.url) {
|
if !is_official_launcher_package_url(&manifest_url.url) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Official launcher API returned non-official manifest URL: {}",
|
"官方启动器 API 返回了非官方 manifest URL:{}",
|
||||||
manifest_url.url
|
manifest_url.url
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -166,9 +162,7 @@ impl OfficialLauncherBootstrapService {
|
|||||||
manifest_url: &str,
|
manifest_url: &str,
|
||||||
) -> Result<YostarJpLauncherRemoteManifest, String> {
|
) -> Result<YostarJpLauncherRemoteManifest, String> {
|
||||||
if !is_official_launcher_package_url(manifest_url) {
|
if !is_official_launcher_package_url(manifest_url) {
|
||||||
return Err(format!(
|
return Err(format!("拒绝拉取非官方启动器 manifest URL:{manifest_url}"));
|
||||||
"Refusing to fetch non-official launcher manifest URL: {manifest_url}"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = self.run_curl_with_retry(
|
let output = self.run_curl_with_retry(
|
||||||
@@ -186,16 +180,16 @@ impl OfficialLauncherBootstrapService {
|
|||||||
|output| {
|
|output| {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
format!(
|
format!(
|
||||||
"curl failed for launcher manifest {manifest_url}: {}",
|
"curl 拉取启动器 manifest 失败 {manifest_url}:{}",
|
||||||
stderr.trim()
|
stderr.trim()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let manifest: YostarJpLauncherRemoteManifest = serde_json::from_slice(&output.stdout)
|
let manifest: YostarJpLauncherRemoteManifest = serde_json::from_slice(&output.stdout)
|
||||||
.map_err(|error| format!("Failed to parse official launcher manifest: {error}"))?;
|
.map_err(|error| format!("解析官方启动器 manifest 失败:{error}"))?;
|
||||||
if manifest.files.is_empty() {
|
if manifest.files.is_empty() {
|
||||||
return Err("Official launcher remote manifest has no files".into());
|
return Err("官方启动器远端 manifest 没有文件条目".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(manifest)
|
Ok(manifest)
|
||||||
@@ -246,22 +240,22 @@ impl OfficialLauncherBootstrapService {
|
|||||||
},
|
},
|
||||||
|output| {
|
|output| {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
format!("curl failed for {url}: {}", stderr.trim())
|
format!("curl 拉取失败 {url}:{}", stderr.trim())
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let envelope: LauncherEnvelope = serde_json::from_slice(&output.stdout)
|
let envelope: LauncherEnvelope = serde_json::from_slice(&output.stdout)
|
||||||
.map_err(|error| format!("Failed to parse official launcher API response: {error}"))?;
|
.map_err(|error| format!("解析官方启动器 API 响应失败:{error}"))?;
|
||||||
|
|
||||||
if envelope.code != 200 {
|
if envelope.code != 200 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Official launcher API returned code {}: {}",
|
"官方启动器 API 返回错误码 {}:{}",
|
||||||
envelope.code,
|
envelope.code,
|
||||||
envelope
|
envelope
|
||||||
.message
|
.message
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or(envelope.msg.as_deref())
|
.or(envelope.msg.as_deref())
|
||||||
.unwrap_or("<no message>")
|
.unwrap_or("<无消息>")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,20 +274,20 @@ impl OfficialLauncherBootstrapService {
|
|||||||
Ok(output) if output.status.success() => return Ok(output),
|
Ok(output) if output.status.success() => return Ok(output),
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
last_error = Some(format!(
|
last_error = Some(format!(
|
||||||
"attempt {attempt}/{attempts}: {}",
|
"第 {attempt}/{attempts} 次尝试失败:{}",
|
||||||
failure_message(&output)
|
failure_message(&output)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
last_error = Some(format!(
|
last_error = Some(format!(
|
||||||
"attempt {attempt}/{attempts}: Failed to launch curl command {}: {error}",
|
"第 {attempt}/{attempts} 次尝试失败:启动 curl 命令失败 {}:{error}",
|
||||||
self.curl_command
|
self.curl_command
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(last_error.unwrap_or_else(|| format!("curl command {} did not run", self.curl_command)))
|
Err(last_error.unwrap_or_else(|| format!("curl 命令未执行:{}", self.curl_command)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +309,7 @@ pub struct LauncherEnvelope {
|
|||||||
/// Builds an official launcher API URL from a path.
|
/// Builds an official launcher API URL from a path.
|
||||||
pub fn launcher_api_url(path: &str) -> Result<String, String> {
|
pub fn launcher_api_url(path: &str) -> Result<String, String> {
|
||||||
if !path.starts_with('/') || path.contains("..") || path.contains('\\') {
|
if !path.starts_with('/') || path.contains("..") || path.contains('\\') {
|
||||||
return Err(format!("Invalid official launcher API path: {path}"));
|
return Err(format!("官方启动器 API 路径无效:{path}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(format!("{YOSTAR_JP_LAUNCHER_API_ROOT}{path}"))
|
Ok(format!("{YOSTAR_JP_LAUNCHER_API_ROOT}{path}"))
|
||||||
@@ -337,7 +331,7 @@ pub fn is_official_launcher_package_url(url: &str) -> bool {
|
|||||||
/// relative package path.
|
/// relative package path.
|
||||||
pub fn launcher_package_url(cdn_root: &str, file_path: &str) -> Result<String, String> {
|
pub fn launcher_package_url(cdn_root: &str, file_path: &str) -> Result<String, String> {
|
||||||
if !is_official_launcher_package_url(cdn_root) {
|
if !is_official_launcher_package_url(cdn_root) {
|
||||||
return Err(format!("Launcher CDN root is not official: {cdn_root}"));
|
return Err(format!("启动器 CDN 根地址不是官方地址:{cdn_root}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_launcher_package_path(file_path)?;
|
validate_launcher_package_path(file_path)?;
|
||||||
@@ -368,15 +362,15 @@ fn normalize_relative_path(path: &str) -> String {
|
|||||||
|
|
||||||
fn validate_launcher_package_path(path: &str) -> Result<(), String> {
|
fn validate_launcher_package_path(path: &str) -> Result<(), String> {
|
||||||
if path.is_empty() || path.starts_with('/') || path.starts_with('\\') {
|
if path.is_empty() || path.starts_with('/') || path.starts_with('\\') {
|
||||||
return Err(format!("Invalid launcher package path: {path}"));
|
return Err(format!("启动器包路径无效:{path}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
for segment in path.split('/') {
|
for segment in path.split('/') {
|
||||||
if segment.is_empty() || segment == "." || segment == ".." {
|
if segment.is_empty() || segment == "." || segment == ".." {
|
||||||
return Err(format!("Invalid launcher package path: {path}"));
|
return Err(format!("启动器包路径无效:{path}"));
|
||||||
}
|
}
|
||||||
if segment.contains('\\') || segment.contains('?') || segment.contains('#') {
|
if segment.contains('\\') || segment.contains('?') || segment.contains('#') {
|
||||||
return Err(format!("Invalid launcher package path: {path}"));
|
return Err(format!("启动器包路径无效:{path}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,14 +388,14 @@ pub fn launcher_authorization_header(
|
|||||||
timestamp: Option<u64>,
|
timestamp: Option<u64>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
if launcher_version.is_empty() {
|
if launcher_version.is_empty() {
|
||||||
return Err("Launcher version must not be empty".into());
|
return Err("启动器版本不能为空".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = match timestamp {
|
let timestamp = match timestamp {
|
||||||
Some(timestamp) => timestamp,
|
Some(timestamp) => timestamp,
|
||||||
None => SystemTime::now()
|
None => SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map_err(|error| format!("System time is before Unix epoch: {error}"))?
|
.map_err(|error| format!("系统时间早于 Unix epoch:{error}"))?
|
||||||
.as_secs(),
|
.as_secs(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -411,7 +405,7 @@ pub fn launcher_authorization_header(
|
|||||||
"version": launcher_version,
|
"version": launcher_version,
|
||||||
});
|
});
|
||||||
let head_json = serde_json::to_string(&head)
|
let head_json = serde_json::to_string(&head)
|
||||||
.map_err(|error| format!("Failed to serialize launcher auth head: {error}"))?;
|
.map_err(|error| format!("序列化启动器认证 head 失败:{error}"))?;
|
||||||
let mut hasher = Md5::new();
|
let mut hasher = Md5::new();
|
||||||
hasher.update(head_json.as_bytes());
|
hasher.update(head_json.as_bytes());
|
||||||
hasher.update(body.as_bytes());
|
hasher.update(body.as_bytes());
|
||||||
@@ -422,7 +416,7 @@ pub fn launcher_authorization_header(
|
|||||||
"head": head,
|
"head": head,
|
||||||
"sign": sign,
|
"sign": sign,
|
||||||
}))
|
}))
|
||||||
.map_err(|error| format!("Failed to serialize launcher auth header: {error}"))
|
.map_err(|error| format!("序列化启动器认证 header 失败:{error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ impl Default for OfficialUpdateConfig {
|
|||||||
launcher_version: "1.7.2".to_string(),
|
launcher_version: "1.7.2".to_string(),
|
||||||
auto_discover: false,
|
auto_discover: false,
|
||||||
platforms: None,
|
platforms: None,
|
||||||
output_root: PathBuf::from("official_update_output"),
|
output_root: PathBuf::from("./bat-resources"),
|
||||||
snapshot_path: None,
|
snapshot_path: None,
|
||||||
curl_command: PathBuf::from("curl"),
|
curl_command: PathBuf::from("curl"),
|
||||||
unzip_command: PathBuf::from("unzip"),
|
unzip_command: PathBuf::from("unzip"),
|
||||||
@@ -442,7 +442,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"start",
|
"start",
|
||||||
format!(
|
format!(
|
||||||
"starting official resource sync: output={} dry_run={} auto_discover={}",
|
"开始官方资源同步:资源目录={} 试运行={} 自动发现={}",
|
||||||
config.output_root.display(),
|
config.output_root.display(),
|
||||||
config.dry_run,
|
config.dry_run,
|
||||||
config.auto_discover
|
config.auto_discover
|
||||||
@@ -451,18 +451,15 @@ impl OfficialUpdateService {
|
|||||||
check_shutdown_requested(&mut should_cancel)?;
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
|
|
||||||
let _lock = if config.dry_run {
|
let _lock = if config.dry_run {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new("lock", "试运行:跳过状态锁"));
|
||||||
"lock",
|
|
||||||
"dry-run: skipping state lock",
|
|
||||||
));
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"lock",
|
"lock",
|
||||||
format!("acquiring state lock {}", config.lock_path().display()),
|
format!("获取资源目录状态锁 {}", config.lock_path().display()),
|
||||||
));
|
));
|
||||||
let lock = OfficialUpdateLock::acquire(config)?;
|
let lock = OfficialUpdateLock::acquire(config)?;
|
||||||
progress(OfficialUpdateProgress::new("lock", "state lock acquired"));
|
progress(OfficialUpdateProgress::new("lock", "资源目录状态锁已获取"));
|
||||||
Some(lock)
|
Some(lock)
|
||||||
};
|
};
|
||||||
check_shutdown_requested(&mut should_cancel)?;
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
@@ -483,7 +480,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"bootstrap",
|
"bootstrap",
|
||||||
format!(
|
format!(
|
||||||
"auto-discover enabled; launcher_version={} cache={}",
|
"启用自动发现;启动器版本={} 缓存={}",
|
||||||
config.launcher_version,
|
config.launcher_version,
|
||||||
bootstrap_cache_path.display()
|
bootstrap_cache_path.display()
|
||||||
),
|
),
|
||||||
@@ -500,7 +497,7 @@ impl OfficialUpdateService {
|
|||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"bootstrap",
|
"bootstrap",
|
||||||
"auto-discover disabled; using explicit metadata inputs",
|
"未启用自动发现;使用显式元数据输入",
|
||||||
));
|
));
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -515,7 +512,7 @@ impl OfficialUpdateService {
|
|||||||
.map(|bootstrap| bootstrap.launcher_metadata.game_latest_version.clone())
|
.map(|bootstrap| bootstrap.launcher_metadata.game_latest_version.clone())
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
anyhow::anyhow!("missing app version; pass it explicitly or enable auto-discover")
|
anyhow::anyhow!("缺少应用版本;请显式传入 --app-version 或启用 --auto-discover")
|
||||||
})?;
|
})?;
|
||||||
let connection_group = config
|
let connection_group = config
|
||||||
.connection_group
|
.connection_group
|
||||||
@@ -526,14 +523,12 @@ impl OfficialUpdateService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!("缺少连接组;请显式传入 --connection-group 或启用 --auto-discover")
|
||||||
"missing connection group; pass it explicitly or enable auto-discover"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"metadata",
|
"metadata",
|
||||||
format!(
|
format!(
|
||||||
"using app_version={} connection_group={} platforms={}",
|
"使用应用版本={} 连接组={} 平台={}",
|
||||||
app_version,
|
app_version,
|
||||||
connection_group,
|
connection_group,
|
||||||
platforms_label(platforms)
|
platforms_label(platforms)
|
||||||
@@ -543,35 +538,30 @@ impl OfficialUpdateService {
|
|||||||
let server_info_bytes = if let Some(source) = config.server_info_source.as_ref() {
|
let server_info_bytes = if let Some(source) = config.server_info_source.as_ref() {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"server-info",
|
"server-info",
|
||||||
format!(
|
format!("从 {} 读取服务器信息", server_info_source_label(source)),
|
||||||
"loading server-info from {}",
|
|
||||||
server_info_source_label(source)
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
load_server_info(source, &fetcher)?
|
load_server_info(source, &fetcher)?
|
||||||
} else {
|
} else {
|
||||||
let bootstrap = bootstrap.as_ref().ok_or_else(|| {
|
let bootstrap = bootstrap.as_ref().ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"missing server-info source; pass one explicitly or enable auto-discover"
|
"缺少服务器信息来源;请显式传入 --server-info-* 或启用 --auto-discover"
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let url = bootstrap
|
let url = bootstrap
|
||||||
.game_main_config
|
.game_main_config
|
||||||
.server_info_data_url
|
.server_info_data_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| anyhow::anyhow!("官方 GameMainConfig 中没有 ServerInfoDataUrl"))?;
|
||||||
anyhow::anyhow!("official GameMainConfig has no ServerInfoDataUrl")
|
|
||||||
})?;
|
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"server-info",
|
"server-info",
|
||||||
format!("fetching discovered server-info {url}"),
|
format!("拉取自动发现的服务器信息 {url}"),
|
||||||
));
|
));
|
||||||
fetcher.fetch_bytes(url).map_err(anyhow::Error::msg)?
|
fetcher.fetch_bytes(url).map_err(anyhow::Error::msg)?
|
||||||
};
|
};
|
||||||
|
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"server-info",
|
"server-info",
|
||||||
format!("parsing server-info bytes={}", server_info_bytes.len()),
|
format!("解析服务器信息,字节数={}", server_info_bytes.len()),
|
||||||
));
|
));
|
||||||
check_shutdown_requested(&mut should_cancel)?;
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
let server_info = YostarJpServerInfo::from_slice(&server_info_bytes)?;
|
let server_info = YostarJpServerInfo::from_slice(&server_info_bytes)?;
|
||||||
@@ -581,7 +571,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"discovery",
|
"discovery",
|
||||||
format!(
|
format!(
|
||||||
"resolved addressables_root={} endpoints={}",
|
"解析到 Addressables 根={} endpoint 数={}",
|
||||||
current_snapshot.addressables_root,
|
current_snapshot.addressables_root,
|
||||||
current_snapshot.endpoints.len()
|
current_snapshot.endpoints.len()
|
||||||
),
|
),
|
||||||
@@ -589,7 +579,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"markers",
|
"markers",
|
||||||
format!(
|
format!(
|
||||||
"checking {} remote marker endpoints",
|
"检查 {} 个远端标记 endpoint",
|
||||||
marker_endpoint_count(¤t_snapshot)
|
marker_endpoint_count(¤t_snapshot)
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -607,7 +597,7 @@ impl OfficialUpdateService {
|
|||||||
check_shutdown_requested(&mut should_cancel)?;
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"snapshot",
|
"snapshot",
|
||||||
format!("reading previous snapshot {}", snapshot_path.display()),
|
format!("读取上次快照 {}", snapshot_path.display()),
|
||||||
));
|
));
|
||||||
let previous_snapshot = read_snapshot(&snapshot_path)?;
|
let previous_snapshot = read_snapshot(&snapshot_path)?;
|
||||||
let previous_base_snapshot = previous_snapshot
|
let previous_base_snapshot = previous_snapshot
|
||||||
@@ -623,13 +613,13 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"decision",
|
"decision",
|
||||||
format!(
|
format!(
|
||||||
"remote decision={:?} force={} remote_should_download={}",
|
"远端决策={:?} 强制刷新={} 远端需要下载={}",
|
||||||
sync_plan.decision, config.force, remote_should_download
|
sync_plan.decision, config.force, remote_should_download
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"plan",
|
"plan",
|
||||||
"building official pull plan from latest seed catalogs",
|
"根据最新种子目录构建官方拉取计划",
|
||||||
));
|
));
|
||||||
let pull_plan = build_pull_plan(
|
let pull_plan = build_pull_plan(
|
||||||
&server_info,
|
&server_info,
|
||||||
@@ -643,7 +633,7 @@ impl OfficialUpdateService {
|
|||||||
let url_count = pull_plan.all_urls().map_err(anyhow::Error::msg)?.len();
|
let url_count = pull_plan.all_urls().map_err(anyhow::Error::msg)?.len();
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"plan",
|
"plan",
|
||||||
format!("pull plan contains {url_count} official URLs"),
|
format!("拉取计划包含 {url_count} 个官方 URL"),
|
||||||
));
|
));
|
||||||
let local_state = fetcher
|
let local_state = fetcher
|
||||||
.local_resource_state(&pull_plan)
|
.local_resource_state(&pull_plan)
|
||||||
@@ -652,7 +642,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"local-state",
|
"local-state",
|
||||||
format!(
|
format!(
|
||||||
"manifest_entries={} existing_files={} has_local_resources={}",
|
"manifest 条目={} 已存在文件={} 是否已有本地资源={}",
|
||||||
local_state.manifest_entry_count,
|
local_state.manifest_entry_count,
|
||||||
local_state.existing_file_count,
|
local_state.existing_file_count,
|
||||||
has_local_resources
|
has_local_resources
|
||||||
@@ -663,7 +653,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"audit",
|
"audit",
|
||||||
format!(
|
format!(
|
||||||
"auditing local download manifest {}",
|
"审计本地下载 manifest {}",
|
||||||
fetcher.download_manifest_path().display()
|
fetcher.download_manifest_path().display()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -675,13 +665,13 @@ impl OfficialUpdateService {
|
|||||||
} else if config.audit_local {
|
} else if config.audit_local {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"audit",
|
"audit",
|
||||||
"no local resources found; skipping local audit before first full pull",
|
"未发现本地资源;首次全量拉取前跳过本地审计",
|
||||||
));
|
));
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"audit",
|
"audit",
|
||||||
"local manifest audit disabled",
|
"本地 manifest 审计已关闭",
|
||||||
));
|
));
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -703,14 +693,14 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"audit",
|
"audit",
|
||||||
format!(
|
format!(
|
||||||
"local manifest verified={} repair_needed={}",
|
"本地 manifest 已校验={} 需要修复={}",
|
||||||
local_manifest_verified_count, local_manifest_repair_needed_count
|
local_manifest_verified_count, local_manifest_repair_needed_count
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"decision",
|
"decision",
|
||||||
format!(
|
format!(
|
||||||
"final should_download={} repair_needed={} initial_pull_needed={}",
|
"最终决策 需要下载={} 需要修复={} 首次拉取={}",
|
||||||
should_download, repair_needed, initial_pull_needed
|
should_download, repair_needed, initial_pull_needed
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -760,7 +750,7 @@ impl OfficialUpdateService {
|
|||||||
if !should_download {
|
if !should_download {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"finish",
|
"finish",
|
||||||
"up to date; no download needed",
|
"资源已是最新;无需下载",
|
||||||
));
|
));
|
||||||
return Ok(report);
|
return Ok(report);
|
||||||
}
|
}
|
||||||
@@ -773,20 +763,20 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"dry-run",
|
"dry-run",
|
||||||
format!(
|
format!(
|
||||||
"dry-run plan generated {} URLs",
|
"试运行已生成 {} 个 URL",
|
||||||
report.download_url_count.unwrap_or(0)
|
report.download_url_count.unwrap_or(0)
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"dry-run",
|
"dry-run",
|
||||||
"dry-run detected pending download; full URL plan disabled",
|
"试运行检测到需要下载;未启用完整 URL 计划输出",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
report.update_status = OfficialUpdateStatus::WouldDownload;
|
report.update_status = OfficialUpdateStatus::WouldDownload;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"finish",
|
"finish",
|
||||||
"dry-run finished without writing state",
|
"试运行完成,未写入状态",
|
||||||
));
|
));
|
||||||
return Ok(report);
|
return Ok(report);
|
||||||
}
|
}
|
||||||
@@ -795,7 +785,7 @@ impl OfficialUpdateService {
|
|||||||
let download_url_count = pull_plan.all_urls().map_err(anyhow::Error::msg)?.len();
|
let download_url_count = pull_plan.all_urls().map_err(anyhow::Error::msg)?.len();
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"download",
|
"download",
|
||||||
format!("downloading or reusing {download_url_count} official URLs"),
|
format!("下载或复用 {download_url_count} 个官方 URL"),
|
||||||
));
|
));
|
||||||
let pull_report = fetcher
|
let pull_report = fetcher
|
||||||
.pull_with_progress_and_cancellation(
|
.pull_with_progress_and_cancellation(
|
||||||
@@ -809,7 +799,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"download",
|
"download",
|
||||||
format!(
|
format!(
|
||||||
"download phase finished: downloaded={} resumed={} skipped={} transferred_bytes={}",
|
"下载阶段完成:已下载={} 已续传={} 已复用={} 本轮传输字节={}",
|
||||||
pull_report.downloaded_count(),
|
pull_report.downloaded_count(),
|
||||||
pull_report.resumed_count(),
|
pull_report.resumed_count(),
|
||||||
pull_report.skipped_count(),
|
pull_report.skipped_count(),
|
||||||
@@ -818,7 +808,7 @@ impl OfficialUpdateService {
|
|||||||
));
|
));
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"audit",
|
"audit",
|
||||||
"running final local manifest audit",
|
"执行最终本地 manifest 审计",
|
||||||
));
|
));
|
||||||
check_shutdown_requested(&mut should_cancel)?;
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
let final_audit = fetcher
|
let final_audit = fetcher
|
||||||
@@ -826,7 +816,7 @@ impl OfficialUpdateService {
|
|||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"snapshot",
|
"snapshot",
|
||||||
format!("writing snapshot {}", snapshot_path.display()),
|
format!("写入快照 {}", snapshot_path.display()),
|
||||||
));
|
));
|
||||||
write_snapshot(&snapshot_path, ¤t_update_snapshot)?;
|
write_snapshot(&snapshot_path, ¤t_update_snapshot)?;
|
||||||
|
|
||||||
@@ -845,7 +835,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"finish",
|
"finish",
|
||||||
format!(
|
format!(
|
||||||
"sync finished: resources={} official_hashes_verified={}",
|
"同步完成:资源数={} 官方 hash 已校验={}",
|
||||||
report.resource_count.unwrap_or(0),
|
report.resource_count.unwrap_or(0),
|
||||||
report.official_seed_hash_verified_count
|
report.official_seed_hash_verified_count
|
||||||
),
|
),
|
||||||
@@ -870,7 +860,7 @@ fn build_pull_plan(
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"plan",
|
"plan",
|
||||||
format!(
|
format!(
|
||||||
"discovery plan resolved: connection_group={} app_version={} endpoints={}",
|
"发现计划已解析:连接组={} 应用版本={} endpoint 数={}",
|
||||||
discovery.connection_group_name,
|
discovery.connection_group_name,
|
||||||
discovery.app_version,
|
discovery.app_version,
|
||||||
discovery.endpoints.len()
|
discovery.endpoints.len()
|
||||||
@@ -881,7 +871,7 @@ fn build_pull_plan(
|
|||||||
check_shutdown_requested(should_cancel)?;
|
check_shutdown_requested(should_cancel)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"inventory",
|
"inventory",
|
||||||
"parsing seed catalogs into download inventory",
|
"正在把种子目录解析为下载清单",
|
||||||
));
|
));
|
||||||
let inventory = build_inventory_from_seed_catalogs(&seed_catalogs, platforms)?;
|
let inventory = build_inventory_from_seed_catalogs(&seed_catalogs, platforms)?;
|
||||||
Ok(build_official_pull_plan_for_platform_inventory(
|
Ok(build_official_pull_plan_for_platform_inventory(
|
||||||
@@ -921,7 +911,7 @@ fn collect_endpoint_markers(
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"marker",
|
"marker",
|
||||||
format!(
|
format!(
|
||||||
"fetching {} marker{} {}",
|
"拉取 {} 标记{} {}",
|
||||||
endpoint_kind_label(endpoint.kind),
|
endpoint_kind_label(endpoint.kind),
|
||||||
platform_suffix(endpoint.platform),
|
platform_suffix(endpoint.platform),
|
||||||
endpoint.url
|
endpoint.url
|
||||||
@@ -946,9 +936,7 @@ fn collect_endpoint_markers(
|
|||||||
|
|
||||||
fn check_shutdown_requested(should_cancel: &mut dyn FnMut() -> bool) -> anyhow::Result<()> {
|
fn check_shutdown_requested(should_cancel: &mut dyn FnMut() -> bool) -> anyhow::Result<()> {
|
||||||
if should_cancel() {
|
if should_cancel() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!("官方资源更新已被停止请求中断"));
|
||||||
"official update interrupted by shutdown request"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -978,11 +966,11 @@ fn marker_endpoint_count(snapshot: &YostarJpSyncSnapshot) -> usize {
|
|||||||
|
|
||||||
fn server_info_source_label(source: &OfficialServerInfoSource) -> String {
|
fn server_info_source_label(source: &OfficialServerInfoSource) -> String {
|
||||||
match source {
|
match source {
|
||||||
OfficialServerInfoSource::LocalPath(path) => format!("local path {}", path.display()),
|
OfficialServerInfoSource::LocalPath(path) => format!("本地路径 {}", path.display()),
|
||||||
OfficialServerInfoSource::OfficialFile(file_name) => {
|
OfficialServerInfoSource::OfficialFile(file_name) => {
|
||||||
format!("official file {file_name}")
|
format!("官方文件 {file_name}")
|
||||||
}
|
}
|
||||||
OfficialServerInfoSource::OfficialUrl(url) => format!("official URL {url}"),
|
OfficialServerInfoSource::OfficialUrl(url) => format!("官方 URL {url}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1017,17 +1005,14 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
|
|||||||
match event.kind {
|
match event.kind {
|
||||||
OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new(
|
OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new(
|
||||||
"download",
|
"download",
|
||||||
format!("({}/{}) started {}", event.index, event.total, event.url),
|
format!("({}/{}) 开始 {}", event.index, event.total, event.url),
|
||||||
),
|
),
|
||||||
OfficialResourcePullProgressKind::Finished => {
|
OfficialResourcePullProgressKind::Finished => {
|
||||||
let status = event
|
let status = event.status.map(localized_pull_status).unwrap_or("未知");
|
||||||
.status
|
|
||||||
.map(|status| status.as_str())
|
|
||||||
.unwrap_or("unknown");
|
|
||||||
OfficialUpdateProgress::new(
|
OfficialUpdateProgress::new(
|
||||||
"download",
|
"download",
|
||||||
format!(
|
format!(
|
||||||
"({}/{}) finished status={} bytes={} transferred_bytes={} {}",
|
"({}/{}) 完成 状态={} 文件字节={} 本轮传输字节={} {}",
|
||||||
event.index,
|
event.index,
|
||||||
event.total,
|
event.total,
|
||||||
status,
|
status,
|
||||||
@@ -1040,6 +1025,14 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn localized_pull_status(status: crate::OfficialResourcePullStatus) -> &'static str {
|
||||||
|
match status {
|
||||||
|
crate::OfficialResourcePullStatus::SkippedExisting => "已复用",
|
||||||
|
crate::OfficialResourcePullStatus::Resumed => "已续传",
|
||||||
|
crate::OfficialResourcePullStatus::Downloaded => "已下载",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Computes the extended snapshot delta.
|
/// Computes the extended snapshot delta.
|
||||||
pub fn diff_extended_snapshot(
|
pub fn diff_extended_snapshot(
|
||||||
current: &OfficialUpdateSnapshot,
|
current: &OfficialUpdateSnapshot,
|
||||||
@@ -1074,7 +1067,7 @@ pub fn read_snapshot(path: &Path) -> anyhow::Result<Option<OfficialUpdateSnapsho
|
|||||||
let legacy =
|
let legacy =
|
||||||
serde_json::from_slice::<YostarJpSyncSnapshot>(&data).map_err(|legacy_error| {
|
serde_json::from_slice::<YostarJpSyncSnapshot>(&data).map_err(|legacy_error| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"failed to parse official update snapshot as v2 ({update_error}) or legacy v1 ({legacy_error})"
|
"无法解析官方更新快照:v2 解析失败 ({update_error}),legacy v1 解析也失败 ({legacy_error})"
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(OfficialUpdateSnapshot::new(legacy, Vec::new(), None)))
|
Ok(Some(OfficialUpdateSnapshot::new(legacy, Vec::new(), None)))
|
||||||
@@ -1178,7 +1171,7 @@ fn resolve_bootstrap(
|
|||||||
);
|
);
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"launcher",
|
"launcher",
|
||||||
"fetching official launcher game config and remote manifest",
|
"正在拉取官方启动器游戏配置和远端 manifest",
|
||||||
));
|
));
|
||||||
let (game_config, manifest_url, manifest) = launcher
|
let (game_config, manifest_url, manifest) = launcher
|
||||||
.fetch_latest_remote_manifest()
|
.fetch_latest_remote_manifest()
|
||||||
@@ -1189,14 +1182,14 @@ fn resolve_bootstrap(
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"launcher",
|
"launcher",
|
||||||
format!(
|
format!(
|
||||||
"launcher metadata resolved: latest_version={} manifest_files={}",
|
"启动器元数据已解析:最新版本={} manifest 文件数={}",
|
||||||
launcher_metadata.game_latest_version, launcher_metadata.manifest_file_count
|
launcher_metadata.game_latest_version, launcher_metadata.manifest_file_count
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"bootstrap-cache",
|
"bootstrap-cache",
|
||||||
format!("checking bootstrap cache {}", cache_path.display()),
|
format!("检查启动缓存 {}", cache_path.display()),
|
||||||
));
|
));
|
||||||
if let Some(cache) = read_bootstrap_cache(cache_path)? {
|
if let Some(cache) = read_bootstrap_cache(cache_path)? {
|
||||||
if let Some(game_main_config) =
|
if let Some(game_main_config) =
|
||||||
@@ -1204,7 +1197,7 @@ fn resolve_bootstrap(
|
|||||||
{
|
{
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"bootstrap-cache",
|
"bootstrap-cache",
|
||||||
"cache hit; reusing parsed GameMainConfig",
|
"命中缓存;复用已解析的 GameMainConfig",
|
||||||
));
|
));
|
||||||
return Ok(ResolvedBootstrap {
|
return Ok(ResolvedBootstrap {
|
||||||
launcher_metadata,
|
launcher_metadata,
|
||||||
@@ -1216,7 +1209,7 @@ fn resolve_bootstrap(
|
|||||||
|
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"game-main-config",
|
"game-main-config",
|
||||||
"cache miss; fetching resources.assets and parsing GameMainConfig",
|
"缓存未命中;拉取 resources.assets 并解析 GameMainConfig",
|
||||||
));
|
));
|
||||||
check_shutdown_requested(should_cancel)?;
|
check_shutdown_requested(should_cancel)?;
|
||||||
let bootstrapper = OfficialGameMainConfigBootstrapService::with_commands(
|
let bootstrapper = OfficialGameMainConfigBootstrapService::with_commands(
|
||||||
@@ -1249,12 +1242,12 @@ fn resolve_bootstrap(
|
|||||||
)?;
|
)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"bootstrap-cache",
|
"bootstrap-cache",
|
||||||
format!("bootstrap cache written {}", cache_path.display()),
|
format!("启动缓存已写入 {}", cache_path.display()),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"bootstrap-cache",
|
"bootstrap-cache",
|
||||||
"dry-run: bootstrap cache not written",
|
"试运行:不写入启动缓存",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1287,7 +1280,7 @@ fn fetch_seed_catalogs(
|
|||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"catalog",
|
"catalog",
|
||||||
format!(
|
format!(
|
||||||
"fetching seed catalog {}{} {}",
|
"拉取种子目录 {}{} {}",
|
||||||
endpoint_kind_label(endpoint.kind),
|
endpoint_kind_label(endpoint.kind),
|
||||||
platform_suffix(endpoint.platform),
|
platform_suffix(endpoint.platform),
|
||||||
endpoint.url
|
endpoint.url
|
||||||
@@ -1310,21 +1303,18 @@ fn build_inventory_from_seed_catalogs(
|
|||||||
let table_catalog = catalogs
|
let table_catalog = catalogs
|
||||||
.table_catalog
|
.table_catalog
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| anyhow::anyhow!("official discovery did not include TableCatalog.bytes"))?;
|
.ok_or_else(|| anyhow::anyhow!("官方发现结果缺少 TableCatalog.bytes"))?;
|
||||||
let mut platform_catalogs = Vec::new();
|
let mut platform_catalogs = Vec::new();
|
||||||
|
|
||||||
for platform in platforms {
|
for platform in platforms {
|
||||||
let bundle_packing_info = catalogs.bundle_packing_infos.get(platform).ok_or_else(|| {
|
let bundle_packing_info = catalogs.bundle_packing_infos.get(platform).ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"official discovery did not include {} BundlePackingInfo.bytes",
|
"官方发现结果缺少 {} BundlePackingInfo.bytes",
|
||||||
platform.as_str()
|
platform.as_str()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let media_catalog = catalogs.media_catalogs.get(platform).ok_or_else(|| {
|
let media_catalog = catalogs.media_catalogs.get(platform).ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!("官方发现结果缺少 {} MediaCatalog.bytes", platform.as_str())
|
||||||
"official discovery did not include {} MediaCatalog.bytes",
|
|
||||||
platform.as_str()
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
platform_catalogs.push(YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
platform_catalogs.push(YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
||||||
@@ -1375,7 +1365,7 @@ impl SeedCatalogs {
|
|||||||
fn required_platform(endpoint: &YostarJpResourceEndpoint) -> anyhow::Result<PatchPlatform> {
|
fn required_platform(endpoint: &YostarJpResourceEndpoint) -> anyhow::Result<PatchPlatform> {
|
||||||
endpoint.platform.ok_or_else(|| {
|
endpoint.platform.ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"discovery endpoint {:?} has no platform: {}",
|
"发现 endpoint {:?} 缺少平台:{}",
|
||||||
endpoint.kind,
|
endpoint.kind,
|
||||||
endpoint.url
|
endpoint.url
|
||||||
)
|
)
|
||||||
@@ -1403,13 +1393,13 @@ impl OfficialUpdateLock {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"official update output is locked: {}",
|
"官方资源目录已被锁定 (locked):{}",
|
||||||
path.display()
|
path.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"failed to acquire official update lock {}: {error}",
|
"获取官方资源目录状态锁失败 {}:{error}",
|
||||||
path.display()
|
path.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1417,7 +1407,7 @@ impl OfficialUpdateLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
Err(anyhow::anyhow!(
|
||||||
"failed to acquire official update lock {}",
|
"获取官方资源目录状态锁失败 {}",
|
||||||
path.display()
|
path.display()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -1639,7 +1629,7 @@ mod tests {
|
|||||||
|
|
||||||
let first = OfficialUpdateLock::acquire(&config).unwrap();
|
let first = OfficialUpdateLock::acquire(&config).unwrap();
|
||||||
let second = OfficialUpdateLock::acquire(&config).unwrap_err();
|
let second = OfficialUpdateLock::acquire(&config).unwrap_err();
|
||||||
assert!(second.to_string().contains("locked"));
|
assert!(second.to_string().contains("锁定"));
|
||||||
drop(first);
|
drop(first);
|
||||||
assert!(OfficialUpdateLock::acquire(&config).is_ok());
|
assert!(OfficialUpdateLock::acquire(&config).is_ok());
|
||||||
}
|
}
|
||||||
@@ -1681,7 +1671,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(error.to_string().contains("shutdown request"));
|
assert!(error.to_string().contains("停止请求"));
|
||||||
assert_eq!(checks, 1);
|
assert_eq!(checks, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,9 +180,9 @@ fn official_update_service_emits_human_progress_events() {
|
|||||||
assert!(events.iter().any(|event| event.stage == "catalog"));
|
assert!(events.iter().any(|event| event.stage == "catalog"));
|
||||||
assert!(events.iter().any(|event| event.stage == "dry-run"));
|
assert!(events.iter().any(|event| event.stage == "dry-run"));
|
||||||
assert!(events.iter().any(|event| event.stage == "finish"));
|
assert!(events.iter().any(|event| event.stage == "finish"));
|
||||||
assert!(events.iter().any(|event| event
|
assert!(events
|
||||||
.message
|
.iter()
|
||||||
.contains("pull plan contains 19 official URLs")));
|
.any(|event| event.message.contains("拉取计划包含 19 个官方 URL")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -201,14 +201,14 @@ fn official_update_first_run_pulls_without_pre_audit() {
|
|||||||
assert_eq!(report.skipped_count, 0);
|
assert_eq!(report.skipped_count, 0);
|
||||||
assert_eq!(report.local_manifest_repair_needed_count, 0);
|
assert_eq!(report.local_manifest_repair_needed_count, 0);
|
||||||
assert!(events.iter().any(|event| {
|
assert!(events.iter().any(|event| {
|
||||||
event.stage == "local-state" && event.message.contains("has_local_resources=false")
|
event.stage == "local-state" && event.message.contains("是否已有本地资源=false")
|
||||||
}));
|
}));
|
||||||
assert!(events
|
assert!(events
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| { event.stage == "audit" && event.message.contains("skipping local audit") }));
|
.any(|event| { event.stage == "audit" && event.message.contains("跳过本地审计") }));
|
||||||
assert!(events.iter().any(|event| {
|
assert!(events
|
||||||
event.stage == "decision" && event.message.contains("initial_pull_needed=true")
|
.iter()
|
||||||
}));
|
.any(|event| { event.stage == "decision" && event.message.contains("首次拉取=true") }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -227,13 +227,13 @@ fn official_update_second_run_audits_existing_resources_before_reuse() {
|
|||||||
assert_eq!(report.local_manifest_verified_count, 19);
|
assert_eq!(report.local_manifest_verified_count, 19);
|
||||||
assert_eq!(report.local_manifest_repair_needed_count, 0);
|
assert_eq!(report.local_manifest_repair_needed_count, 0);
|
||||||
assert!(events.iter().any(|event| {
|
assert!(events.iter().any(|event| {
|
||||||
event.stage == "local-state" && event.message.contains("has_local_resources=true")
|
event.stage == "local-state" && event.message.contains("是否已有本地资源=true")
|
||||||
}));
|
}));
|
||||||
assert!(events.iter().any(|event| {
|
assert!(events.iter().any(|event| {
|
||||||
event.stage == "audit" && event.message.contains("auditing local download manifest")
|
event.stage == "audit" && event.message.contains("审计本地下载 manifest")
|
||||||
}));
|
}));
|
||||||
assert!(events.iter().any(|event| {
|
assert!(events.iter().any(|event| {
|
||||||
event.stage == "decision" && event.message.contains("initial_pull_needed=false")
|
event.stage == "decision" && event.message.contains("首次拉取=false")
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user