mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-19 12:06:40 +08:00
fix(release): 收紧分发热路径与事务边界
This commit is contained in:
@@ -100,6 +100,8 @@ struct LocalizedReleaseTransaction {
|
||||
current_target: Option<PathBuf>,
|
||||
previous_state_bytes: Option<Vec<u8>>,
|
||||
new_state: Option<LocalizedVersionState>,
|
||||
#[serde(default)]
|
||||
rollback_backup_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl LocalizedReleaseTransaction {
|
||||
@@ -121,6 +123,7 @@ impl LocalizedReleaseTransaction {
|
||||
current_target: Some(Path::new(LOCALIZED_VERSIONS_DIR).join(release_id)),
|
||||
previous_state_bytes,
|
||||
new_state: None,
|
||||
rollback_backup_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -941,9 +944,33 @@ impl LocalizedPatchService {
|
||||
current_target: manifest.rollback.previous_current_target.clone(),
|
||||
previous_state_bytes: Some(previous_state_bytes),
|
||||
new_state: Some(new_state.clone()),
|
||||
rollback_backup_path: Some(localized_output_root.join(".rollback").join(format!(
|
||||
"{}.{}",
|
||||
current_release_id,
|
||||
std::process::id()
|
||||
))),
|
||||
};
|
||||
write_localized_transaction(localized_output_root, &transaction)?;
|
||||
let mutation_result = (|| -> anyhow::Result<()> {
|
||||
let backup_path = transaction
|
||||
.rollback_backup_path
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("localized rollback 缺少备份路径"))?;
|
||||
ensure_path_within_root(localized_output_root, backup_path)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
ensure_safe_directory_path(
|
||||
backup_path.parent().unwrap_or(localized_output_root),
|
||||
"localized rollback 备份目录",
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
fs::create_dir_all(backup_path.parent().unwrap_or(localized_output_root))?;
|
||||
ensure_safe_directory_path(
|
||||
backup_path.parent().unwrap_or(localized_output_root),
|
||||
"localized rollback 备份目录",
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
fs::rename(&remove_version_path, backup_path)?;
|
||||
update_localized_transaction_phase(localized_output_root, "version_staged")?;
|
||||
restore_current_symlink(
|
||||
localized_output_root,
|
||||
¤t_path,
|
||||
@@ -952,8 +979,18 @@ impl LocalizedPatchService {
|
||||
update_localized_transaction_phase(localized_output_root, "current_switched")?;
|
||||
write_localized_version_state_unlocked(localized_output_root, &new_state)?;
|
||||
update_localized_transaction_phase(localized_output_root, "state_written")?;
|
||||
remove_owned_path(&remove_version_path)?;
|
||||
if let Some(previous_target) = manifest.rollback.previous_current_target.as_deref() {
|
||||
if !current_points_to_version(
|
||||
¤t_path,
|
||||
&localized_output_root.join(previous_target),
|
||||
)? {
|
||||
return Err(anyhow::anyhow!("localized rollback current 最终校验失败"));
|
||||
}
|
||||
} else if fs::symlink_metadata(¤t_path).is_ok() {
|
||||
return Err(anyhow::anyhow!("localized rollback 应移除 current 指针"));
|
||||
}
|
||||
update_localized_transaction_phase(localized_output_root, "version_removed")?;
|
||||
remove_owned_path(backup_path)?;
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(error) = mutation_result {
|
||||
@@ -1216,6 +1253,7 @@ impl LocalizedPatchService {
|
||||
translation_workflow_status,
|
||||
updated_unix_seconds: unix_seconds_now(),
|
||||
};
|
||||
update_localized_transaction_state(&config.localized_output_root, &state)?;
|
||||
write_file_atomic(
|
||||
&state_path,
|
||||
&serde_json::to_vec_pretty(&state)?,
|
||||
@@ -1230,6 +1268,7 @@ impl LocalizedPatchService {
|
||||
¤t_path,
|
||||
&config.unzip_command,
|
||||
)?;
|
||||
update_localized_transaction_phase(&config.localized_output_root, "verified")?;
|
||||
|
||||
Ok(LocalizedPatchReport {
|
||||
version_path,
|
||||
@@ -2139,6 +2178,7 @@ fn verify_published_localized_release(
|
||||
&manifest,
|
||||
unzip_command,
|
||||
)?;
|
||||
verify_localized_distribution_manifest_at(version_path, &manifest.localized_release_id)?;
|
||||
integrity.current_points_to_release = current_points_to_version(current_path, version_path)?;
|
||||
if !integrity.current_points_to_release {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -2147,6 +2187,23 @@ fn verify_published_localized_release(
|
||||
version_path.display()
|
||||
));
|
||||
}
|
||||
let state_path = current_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("localized current 缺少输出根目录"))?
|
||||
.join(LOCALIZED_VERSION_STATE_FILE);
|
||||
let state = read_localized_version_state(
|
||||
current_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("localized current 缺少输出根目录"))?,
|
||||
)?
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 localized version state:{}", state_path.display()))?;
|
||||
if state.current_release_id.as_deref() != Some(manifest.localized_release_id.as_str()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"localized version state 与发布 manifest 不一致:expected={} actual={:?}",
|
||||
manifest.localized_release_id,
|
||||
state.current_release_id
|
||||
));
|
||||
}
|
||||
Ok(integrity)
|
||||
}
|
||||
|
||||
@@ -2289,6 +2346,24 @@ fn update_localized_transaction_phase(
|
||||
write_localized_transaction(localized_output_root, &transaction)
|
||||
}
|
||||
|
||||
fn update_localized_transaction_state(
|
||||
localized_output_root: &Path,
|
||||
state: &LocalizedVersionState,
|
||||
) -> anyhow::Result<()> {
|
||||
let path = localized_output_root.join(LOCALIZED_TRANSACTION_FILE);
|
||||
let Some(bytes) =
|
||||
read_file_no_symlink(&path, "localized release transaction").map_err(anyhow::Error::msg)?
|
||||
else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"localized release transaction 丢失:{}",
|
||||
path.display()
|
||||
));
|
||||
};
|
||||
let mut transaction: LocalizedReleaseTransaction = serde_json::from_slice(&bytes)?;
|
||||
transaction.new_state = Some(state.clone());
|
||||
write_localized_transaction(localized_output_root, &transaction)
|
||||
}
|
||||
|
||||
fn remove_localized_transaction(localized_output_root: &Path) -> anyhow::Result<()> {
|
||||
let path = localized_output_root.join(LOCALIZED_TRANSACTION_FILE);
|
||||
match fs::symlink_metadata(&path) {
|
||||
@@ -2341,15 +2416,14 @@ fn recover_localized_transaction(localized_output_root: &Path) -> anyhow::Result
|
||||
== Some(expected)
|
||||
});
|
||||
let publish_committed = transaction.operation == "publish"
|
||||
&& transaction.phase == "verified"
|
||||
&& transaction.version_path.is_dir()
|
||||
&& current_matches
|
||||
&& read_localized_version_state(localized_output_root)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|state| state.current_release_id)
|
||||
.is_some_and(|id| id == transaction.release_id);
|
||||
let rollback_committed =
|
||||
transaction.operation == "rollback" && current_matches && state_matches;
|
||||
&& state_matches;
|
||||
let rollback_committed = transaction.operation == "rollback"
|
||||
&& transaction.phase == "version_removed"
|
||||
&& current_matches
|
||||
&& state_matches;
|
||||
|
||||
if publish_committed {
|
||||
if let Some(staging) = transaction.staging_path.as_deref() {
|
||||
@@ -2357,6 +2431,9 @@ fn recover_localized_transaction(localized_output_root: &Path) -> anyhow::Result
|
||||
}
|
||||
} else if rollback_committed {
|
||||
remove_owned_path(&transaction.version_path)?;
|
||||
if let Some(backup) = transaction.rollback_backup_path.as_deref() {
|
||||
remove_owned_path(backup)?;
|
||||
}
|
||||
} else {
|
||||
if let Some(target) = transaction.previous_current_target.as_deref() {
|
||||
let target_path = localized_output_root.join(target);
|
||||
@@ -2372,6 +2449,14 @@ fn recover_localized_transaction(localized_output_root: &Path) -> anyhow::Result
|
||||
}
|
||||
remove_owned_path(&transaction.version_path)?;
|
||||
}
|
||||
if transaction.operation == "rollback" {
|
||||
if let Some(backup) = transaction.rollback_backup_path.as_deref() {
|
||||
if fs::symlink_metadata(backup).is_ok() {
|
||||
remove_owned_path(&transaction.version_path)?;
|
||||
fs::rename(backup, &transaction.version_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(previous_state) = transaction.previous_state_bytes.as_deref() {
|
||||
write_file_atomic(
|
||||
&localized_output_root.join(LOCALIZED_VERSION_STATE_FILE),
|
||||
@@ -3533,6 +3618,98 @@ mod tests {
|
||||
assert!(!localized_output_transaction_pending(&root).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn publish_recovery_requires_verified_phase_before_roll_forward() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
for (phase, new_is_current, should_keep_new) in [
|
||||
("prepared", false, false),
|
||||
("version_published", false, false),
|
||||
("current_switched", true, false),
|
||||
("state_written", true, false),
|
||||
("verification_failed", true, false),
|
||||
("verification_succeeded", true, false),
|
||||
("verified", true, true),
|
||||
] {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let root = temp.path().join("localized");
|
||||
let old = root.join(LOCALIZED_VERSIONS_DIR).join("old");
|
||||
let new = root.join(LOCALIZED_VERSIONS_DIR).join("new");
|
||||
let staging = root.join(LOCALIZED_STAGING_DIR).join("new");
|
||||
fs::create_dir_all(&old).unwrap();
|
||||
fs::create_dir_all(&new).unwrap();
|
||||
fs::create_dir_all(&staging).unwrap();
|
||||
symlink(
|
||||
Path::new(LOCALIZED_VERSIONS_DIR).join(if new_is_current { "new" } else { "old" }),
|
||||
root.join(LOCALIZED_CURRENT_LINK),
|
||||
)
|
||||
.unwrap();
|
||||
let old_state = LocalizedVersionState {
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: "official-old".to_string(),
|
||||
current_release_id: Some("old".to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 1,
|
||||
};
|
||||
let new_state = LocalizedVersionState {
|
||||
current_release_id: Some("new".to_string()),
|
||||
updated_unix_seconds: 2,
|
||||
..old_state.clone()
|
||||
};
|
||||
let old_state_bytes = serde_json::to_vec_pretty(&old_state).unwrap();
|
||||
let state_bytes = if new_is_current {
|
||||
serde_json::to_vec_pretty(&new_state).unwrap()
|
||||
} else {
|
||||
old_state_bytes.clone()
|
||||
};
|
||||
write_file_atomic(
|
||||
&root.join(LOCALIZED_VERSION_STATE_FILE),
|
||||
&state_bytes,
|
||||
STATE_FILE_MODE,
|
||||
"test state",
|
||||
)
|
||||
.unwrap();
|
||||
let mut transaction = LocalizedReleaseTransaction::publish(
|
||||
"new",
|
||||
new.clone(),
|
||||
staging.clone(),
|
||||
Some(PathBuf::from("versions/old")),
|
||||
Some(old_state_bytes),
|
||||
);
|
||||
transaction.phase = phase.to_string();
|
||||
transaction.new_state = Some(new_state.clone());
|
||||
write_localized_transaction(&root, &transaction).unwrap();
|
||||
|
||||
recover_localized_transaction(&root).unwrap();
|
||||
|
||||
if should_keep_new {
|
||||
assert_eq!(
|
||||
fs::read_link(root.join(LOCALIZED_CURRENT_LINK)).unwrap(),
|
||||
PathBuf::from("versions/new")
|
||||
);
|
||||
assert_eq!(
|
||||
read_localized_version_state(&root).unwrap(),
|
||||
Some(new_state)
|
||||
);
|
||||
assert!(new.exists());
|
||||
} else {
|
||||
assert_eq!(
|
||||
fs::read_link(root.join(LOCALIZED_CURRENT_LINK)).unwrap(),
|
||||
PathBuf::from("versions/old")
|
||||
);
|
||||
assert_eq!(
|
||||
read_localized_version_state(&root).unwrap(),
|
||||
Some(old_state)
|
||||
);
|
||||
assert!(!new.exists());
|
||||
}
|
||||
assert!(!staging.exists());
|
||||
assert!(!localized_output_transaction_pending(&root).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn push_i16_le(data: &mut Vec<u8>, value: i16) {
|
||||
data.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user