mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 11:34:59 +08:00
feat(assetbundle): 完善官方资源解析与双目录发布
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
use crate::curl_transfer::{resolve_curl_proxy, CurlProxyConfig};
|
||||
use crate::path_security::{
|
||||
ensure_path_within_root, ensure_safe_directory_path, ensure_safe_file_target,
|
||||
ensure_path_within_root, ensure_safe_directory_path, ensure_safe_file_target, lexical_absolute,
|
||||
read_file_no_symlink, validate_output_root, write_file_atomic, STATE_FILE_MODE,
|
||||
};
|
||||
use crate::{
|
||||
@@ -18,6 +18,7 @@ use crate::{
|
||||
OfficialResourcePullService, YostarJpLauncherGameConfig, YostarJpLauncherManifestUrl,
|
||||
YostarJpLauncherRemoteManifest,
|
||||
};
|
||||
use crate::{OfficialParseCacheService, OfficialParseConfig, OfficialParseSummary};
|
||||
use bat_adapters::official::game_main_config::YostarJpGameMainConfig;
|
||||
use bat_adapters::official::inventory::{
|
||||
YostarJpPlatformCatalogInventory, YostarJpPlatformDownloadInventory,
|
||||
@@ -78,6 +79,8 @@ pub struct OfficialUpdateConfig {
|
||||
pub platforms: Option<Vec<PatchPlatform>>,
|
||||
/// Output root for resources and state files.
|
||||
pub output_root: PathBuf,
|
||||
/// Output root reserved for localized resources generated from official data.
|
||||
pub localized_output_root: PathBuf,
|
||||
/// Optional explicit sync snapshot path.
|
||||
pub snapshot_path: Option<PathBuf>,
|
||||
/// Curl command used by the current infrastructure downloader.
|
||||
@@ -108,6 +111,7 @@ impl Default for OfficialUpdateConfig {
|
||||
auto_discover: false,
|
||||
platforms: None,
|
||||
output_root: PathBuf::from("./bat-resources"),
|
||||
localized_output_root: PathBuf::from("./bat-localized"),
|
||||
snapshot_path: None,
|
||||
curl_command: PathBuf::from("curl"),
|
||||
curl_proxy: CurlProxyConfig::default(),
|
||||
@@ -168,6 +172,26 @@ impl OfficialUpdateStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Publication state for localized resources associated with an official release.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LocalizedReleaseStatus {
|
||||
/// Official resources are published, but localized resources are not.
|
||||
NotLocalized,
|
||||
/// Official resources and localized resources are both published.
|
||||
Localized,
|
||||
}
|
||||
|
||||
impl LocalizedReleaseStatus {
|
||||
/// Returns a stable string label for CLI and JSON callers.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::NotLocalized => "not_localized",
|
||||
Self::Localized => "localized",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of the official JP update state observed at a point in time.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OfficialUpdateSnapshot {
|
||||
@@ -477,6 +501,14 @@ pub struct OfficialUpdateReport {
|
||||
pub platforms: Vec<PatchPlatform>,
|
||||
/// Managed publish root containing `current`, `versions`, and staging.
|
||||
pub output_root: PathBuf,
|
||||
/// Separate root reserved for localized resources.
|
||||
pub localized_output_root: PathBuf,
|
||||
/// Localized publication state for this official resource version.
|
||||
pub localized_release_status: LocalizedReleaseStatus,
|
||||
/// Future localized `current` pointer path under `localized_output_root`.
|
||||
pub localized_current_path: PathBuf,
|
||||
/// Versioned localized release directory when localized resources are published.
|
||||
pub localized_published_version_path: Option<PathBuf>,
|
||||
/// Active resource root used for local audit before this run.
|
||||
pub active_resource_root: PathBuf,
|
||||
/// Atomic `current` pointer path.
|
||||
@@ -543,6 +575,10 @@ pub struct OfficialUpdateReport {
|
||||
pub verification_summary: OfficialVerificationSummary,
|
||||
/// Download manifest path.
|
||||
pub download_manifest: PathBuf,
|
||||
/// Parse-cache path written or refreshed after successful verification.
|
||||
pub parse_cache_path: Option<PathBuf>,
|
||||
/// Post-sync parse-cache summary.
|
||||
pub parse_summary: Option<OfficialParseSummary>,
|
||||
/// Snapshot path written after success.
|
||||
pub snapshot_written: Option<PathBuf>,
|
||||
}
|
||||
@@ -1175,6 +1211,10 @@ impl OfficialUpdateService {
|
||||
addressables_root: current_snapshot.addressables_root.clone(),
|
||||
platforms: platforms.to_vec(),
|
||||
output_root: config.output_root.clone(),
|
||||
localized_output_root: config.localized_output_root.clone(),
|
||||
localized_release_status: LocalizedReleaseStatus::NotLocalized,
|
||||
localized_current_path: config.localized_output_root.join(OFFICIAL_CURRENT_LINK),
|
||||
localized_published_version_path: None,
|
||||
active_resource_root: active_resource_root.clone(),
|
||||
current_path: publish_layout.current_path.clone(),
|
||||
version_state_path: version_state_path.clone(),
|
||||
@@ -1214,6 +1254,8 @@ impl OfficialUpdateService {
|
||||
local_zip_structure_verified_count,
|
||||
),
|
||||
download_manifest: fetcher.download_manifest_path(),
|
||||
parse_cache_path: None,
|
||||
parse_summary: None,
|
||||
snapshot_written: None,
|
||||
};
|
||||
|
||||
@@ -1242,6 +1284,13 @@ impl OfficialUpdateService {
|
||||
&active_resource_root,
|
||||
&snapshot_path,
|
||||
)?;
|
||||
run_post_sync_parse_cache(
|
||||
config,
|
||||
&active_resource_root,
|
||||
&mut report,
|
||||
&mut progress,
|
||||
&mut should_cancel,
|
||||
)?;
|
||||
}
|
||||
progress(OfficialUpdateProgress::new(
|
||||
"finish",
|
||||
@@ -1454,6 +1503,13 @@ impl OfficialUpdateService {
|
||||
format!("资源已发布,但写入版本状态失败(下轮可重试):{error}"),
|
||||
));
|
||||
}
|
||||
run_post_sync_parse_cache(
|
||||
config,
|
||||
&published_version_path,
|
||||
&mut report,
|
||||
&mut progress,
|
||||
&mut should_cancel,
|
||||
)?;
|
||||
|
||||
// 清理未被最新版本状态引用的孤儿 staging 目录(GC 失败仅告警,不影响发布结果)。
|
||||
match read_version_state(&version_state_path) {
|
||||
@@ -1487,6 +1543,46 @@ impl OfficialUpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_post_sync_parse_cache(
|
||||
config: &OfficialUpdateConfig,
|
||||
resource_root: &Path,
|
||||
report: &mut OfficialUpdateReport,
|
||||
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
||||
should_cancel: &mut dyn FnMut() -> bool,
|
||||
) -> anyhow::Result<()> {
|
||||
check_shutdown_requested(should_cancel)?;
|
||||
progress(OfficialUpdateProgress::new(
|
||||
"parse",
|
||||
format!("更新官方资源解析缓存 {}", resource_root.display()),
|
||||
));
|
||||
let parse_config = OfficialParseConfig::new(resource_root, &config.unzip_command);
|
||||
match OfficialParseCacheService::new().run(&parse_config) {
|
||||
Ok(parse_report) => {
|
||||
progress(OfficialUpdateProgress::new(
|
||||
"parse",
|
||||
format!(
|
||||
"解析缓存完成:条目={} 已解析={} 复用={} 不支持={} 失败={} TextAsset={}",
|
||||
parse_report.summary.cache_entry_count,
|
||||
parse_report.summary.parsed_bundle_count,
|
||||
parse_report.summary.skipped_unchanged_count,
|
||||
parse_report.summary.unsupported_count,
|
||||
parse_report.summary.failed_count,
|
||||
parse_report.summary.text_asset_count
|
||||
),
|
||||
));
|
||||
report.parse_cache_path = Some(parse_report.cache_path);
|
||||
report.parse_summary = Some(parse_report.summary);
|
||||
}
|
||||
Err(error) => {
|
||||
progress(OfficialUpdateProgress::new(
|
||||
"parse",
|
||||
format!("解析缓存更新失败(不影响已校验官方资源):{error}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_pull_plan(
|
||||
server_info: &YostarJpServerInfo,
|
||||
connection_group: &str,
|
||||
@@ -1751,7 +1847,10 @@ pub fn diff_extended_snapshot(
|
||||
|
||||
fn validate_update_paths(config: &OfficialUpdateConfig) -> Result<(), String> {
|
||||
validate_output_root(&config.output_root)?;
|
||||
validate_output_root(&config.localized_output_root)?;
|
||||
validate_separate_output_roots(&config.output_root, &config.localized_output_root)?;
|
||||
ensure_safe_directory_path(&config.output_root, "资源输出目录")?;
|
||||
ensure_safe_directory_path(&config.localized_output_root, "汉化输出目录")?;
|
||||
if let Some(snapshot_path) = config.snapshot_path.as_ref() {
|
||||
ensure_path_within_root(&config.output_root, snapshot_path)?;
|
||||
ensure_safe_file_target(&config.output_root, snapshot_path, "官方更新快照")?;
|
||||
@@ -1770,6 +1869,28 @@ fn validate_update_paths(config: &OfficialUpdateConfig) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_separate_output_roots(
|
||||
output_root: &Path,
|
||||
localized_output_root: &Path,
|
||||
) -> Result<(), String> {
|
||||
let official = lexical_absolute(output_root)?;
|
||||
let localized = lexical_absolute(localized_output_root)?;
|
||||
if official == localized {
|
||||
return Err(format!(
|
||||
"官方资源目录和汉化输出目录不能相同:{}",
|
||||
official.display()
|
||||
));
|
||||
}
|
||||
if localized.starts_with(&official) || official.starts_with(&localized) {
|
||||
return Err(format!(
|
||||
"官方资源目录和汉化输出目录不能互相嵌套:官方={} 汉化={}",
|
||||
official.display(),
|
||||
localized.display()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_path_for(config: &OfficialUpdateConfig, resource_root: &Path) -> PathBuf {
|
||||
config
|
||||
.snapshot_path
|
||||
@@ -3412,6 +3533,35 @@ mod tests {
|
||||
assert!(error.to_string().contains("危险路径"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_rejects_same_official_and_localized_output_root() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let root = temp.path().join("resources");
|
||||
let config = OfficialUpdateConfig {
|
||||
output_root: root.clone(),
|
||||
localized_output_root: root,
|
||||
dry_run: true,
|
||||
..OfficialUpdateConfig::default()
|
||||
};
|
||||
|
||||
let error = OfficialUpdateService::new().run(&config).unwrap_err();
|
||||
assert!(error.to_string().contains("不能相同"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_rejects_nested_official_and_localized_output_roots() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let config = OfficialUpdateConfig {
|
||||
output_root: temp.path().join("resources"),
|
||||
localized_output_root: temp.path().join("resources/localized"),
|
||||
dry_run: true,
|
||||
..OfficialUpdateConfig::default()
|
||||
};
|
||||
|
||||
let error = OfficialUpdateService::new().run(&config).unwrap_err();
|
||||
assert!(error.to_string().contains("不能互相嵌套"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_rejects_snapshot_path_escape() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user