Files
Unlucky-Launcher/crates/core/src/platform/atomic.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

80 lines
2.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 原子写:临时文件 → fsync → rename。
//!
//! 出处:`DevDocs/analyse/07-settings-packaging-update.md` §1.6(HMCL `FileUtils.saveSafely`:
//! 写 tmp + `Files.move(REPLACE_EXISTING)`)。配置与下载落盘都该走这里,禁止直接 `fs::write`。
//!
//! `std::fs::rename` 在 Unix 与 Windows 上都是「替换式」语义
//! (Windows 侧 std 用 `MoveFileExW(MOVEFILE_REPLACE_EXISTING)`),因此不需要先删目标文件。
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use super::error::{io_at, PlatformError, PlatformResult};
/// 同进程内的 tmp 名去重计数器。
static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
/// 原子地把 `bytes` 写到 `path`(父目录不存在会创建)。
///
/// # Errors
/// 建目录、写临时文件、fsync、rename 任一步失败都返回 [`PlatformError::Io`],
/// 且失败时会尽力清理临时文件,不留垃圾。
pub fn write_atomic(path: &Path, bytes: &[u8]) -> PlatformResult<()> {
let parent = path.parent().unwrap_or_else(|| Path::new(""));
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(io_at(parent))?;
}
let tmp = tmp_path(path);
let write_result = (|| -> io::Result<()> {
let mut file = std::fs::File::create(&tmp)?;
file.write_all(bytes)?;
file.sync_all()?;
Ok(())
})();
if let Err(source) = write_result {
std::fs::remove_file(&tmp).ok();
return Err(PlatformError::Io { path: tmp, source });
}
if let Err(source) = std::fs::rename(&tmp, path) {
std::fs::remove_file(&tmp).ok();
return Err(PlatformError::Io {
path: path.to_path_buf(),
source,
});
}
Ok(())
}
/// 读文件;不存在返回 `None`,其它错误照原样上报(用于区分「没配过」和「读不了」)。
///
/// # Errors
/// 除 `NotFound` 以外的读失败返回 [`PlatformError::Io`]。
pub fn read_optional(path: &Path) -> PlatformResult<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(PlatformError::Io {
path: path.to_path_buf(),
source,
}),
}
}
/// 同目录下的临时文件名(`launcher.json` → `launcher.json.tmp<pid>-<n>`)。
#[must_use]
pub fn tmp_path(path: &Path) -> PathBuf {
let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let name = path.file_name().map_or_else(
|| String::from("ull"),
|name| name.to_string_lossy().into_owned(),
);
let tmp_name = format!(".{name}.tmp{}-{counter}", std::process::id());
match path.parent() {
Some(parent) => parent.join(tmp_name),
None => PathBuf::from(tmp_name),
}
}