Files
Starlight_Lancher/packages/app-lib/src/api/instance/synced_options/files.rs
T
AxTps ed61a5942c
Sync source to CNB / Sync Git ref (push) Has been cancelled
feat: 跨实例同步移植 WIP 检查点(第1轮+2a+2b+synced_servers)
从 Axolotl 移植跨实例同步子系统,当前进度:

- 第1轮:synced_options/files.rs + mod.rs 骨架,state 层补齐 SyncedOption/InstanceSyncedOptions/CanonicalValue/GameOptionKind 等

- 2a:game_options 的 api_types/options_file/catalog(10文件)

- 2b:game_options 除 resource_pack_options/local_settings_editor 外的全部(settings_editor/write_shared_settings/fullscreen/launch_overrides/instance_support/read_instance_changes/pack_updates/source_selection + locales/5文件);State 补 synced_options_lock/game_locale_indexer/lock_synced_options

- synced_servers:6 文件全部搬入

- 35+20 处 sqlx::query! 宏改写为非宏 query+bind

已知未完成:synced_servers::operations 依赖 synced_options::instance_option_supported(属 orchestration,第5轮),当前编译不通。
2026-10-03 22:18:37 +08:00

339 lines
8.8 KiB
Rust

use crate::state::{InstanceInstallStage, InstanceMetadata, SyncedOption};
use crate::util::io;
use crate::{ErrorKind, State};
use quartz_nbt::NbtCompound;
use sha1_smol::Sha1;
use std::io::Cursor;
use std::path::{Path, PathBuf};
pub(in crate::api::instance) struct SyncCheckpoint {
pub expected_sha1: String,
pub merge_base: Option<Vec<u8>>,
pub source_revision: i64,
pub status: CheckpointStatus,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::api::instance) enum CheckpointStatus {
Pending,
Ready,
}
impl CheckpointStatus {
fn from_str(value: &str) -> Option<Self> {
match value {
"pending" => Some(Self::Pending),
"ready" => Some(Self::Ready),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(in crate::api::instance) enum LinkMode {
Symbolic,
#[cfg(windows)]
Hard,
#[cfg(windows)]
Copy,
}
impl LinkMode {
fn as_str(self) -> &'static str {
match self {
Self::Symbolic => "symbolic",
#[cfg(windows)]
Self::Hard => "hard",
#[cfg(windows)]
Self::Copy => "copy",
}
}
}
pub(in crate::api::instance) fn safe_instance_id(instance_id: &str) -> String {
instance_id.replace([':', '/', '\\'], "_")
}
pub(crate) fn instance_game_dir(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<PathBuf> {
crate::state::instances::commands::instance_content_root(
&state.directories,
&metadata.instance,
)
}
pub(crate) fn synced_options_path(state: &State) -> PathBuf {
state.directories.synced_options_dir()
}
pub(in crate::api::instance) fn sync_files_are_protected(
metadata: &InstanceMetadata,
) -> bool {
matches!(
metadata.instance.install_stage,
InstanceInstallStage::MinecraftInstalling
| InstanceInstallStage::PackInstalling
)
}
pub(in crate::api::instance) async fn instance_is_running(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<bool> {
crate::state::instance_has_running_process(&metadata.instance.id, state)
.await
}
pub(in crate::api::instance) fn option_can_apply_while_running(
option: SyncedOption,
) -> bool {
matches!(
option,
SyncedOption::GameOptions
| SyncedOption::Screenshots
| SyncedOption::ResourcePacks
)
}
pub(in crate::api::instance) fn instance_option_enabled(
metadata: &InstanceMetadata,
option: SyncedOption,
) -> bool {
match option {
SyncedOption::GameOptions => metadata.synced_options.game_options,
SyncedOption::CommandHistory => metadata.synced_options.command_history,
SyncedOption::MultiplayerServers => {
metadata.synced_options.multiplayer_servers
}
SyncedOption::CreativeHotbars => {
metadata.synced_options.creative_hotbars
}
SyncedOption::Screenshots => metadata.synced_options.screenshots,
SyncedOption::ResourcePacks => metadata.synced_options.resource_packs,
SyncedOption::DataPacks => metadata.synced_options.data_packs,
}
}
pub(in crate::api::instance) async fn ensure_link(
source: &Path,
target: &Path,
) -> crate::Result<LinkMode> {
if let Some(parent) = target.parent() {
io::create_dir_all(parent).await?;
}
if tokio::fs::symlink_metadata(target)
.await
.is_ok_and(|metadata| metadata.file_type().is_symlink())
&& tokio::fs::read_link(target)
.await
.is_ok_and(|current| current == source)
{
return Ok(LinkMode::Symbolic);
}
if tokio::fs::symlink_metadata(target).await.is_ok() {
io::remove_file(target).await?;
}
#[cfg(unix)]
{
tokio::fs::symlink(source, target).await?;
Ok(LinkMode::Symbolic)
}
#[cfg(windows)]
{
if tokio::fs::symlink_file(source, target).await.is_ok() {
return Ok(LinkMode::Symbolic);
}
if tokio::fs::hard_link(source, target).await.is_ok() {
return Ok(LinkMode::Hard);
}
io::copy(source, target).await?;
Ok(LinkMode::Copy)
}
}
pub(in crate::api::instance) async fn detach_link(
source: &Path,
target: &Path,
) -> crate::Result<()> {
let target_metadata = tokio::fs::symlink_metadata(target).await.ok();
let contents = if target_metadata.is_some() && target.exists() {
Some(io::read(target).await?)
} else if source.exists() {
Some(io::read(source).await?)
} else {
None
};
if target_metadata.is_some() {
io::remove_file(target).await?;
}
if let Some(contents) = contents {
io::write(target, contents).await?;
}
Ok(())
}
pub(in crate::api::instance) async fn begin_checkpoint(
instance_id: &str,
option: SyncedOption,
variant: &str,
expected_sha1: &str,
merge_base: Option<&[u8]>,
source_revision: i64,
state: &State,
) -> crate::Result<()> {
let option_name = option.as_str();
sqlx::query(
"
INSERT INTO instance_sync_checkpoints
(instance_id, feature, variant, expected_sha1, merge_base,
source_revision, status, link_mode)
VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL)
ON CONFLICT(instance_id, feature, variant) DO UPDATE SET
expected_sha1 = excluded.expected_sha1,
merge_base = excluded.merge_base,
source_revision = excluded.source_revision,
status = 'pending',
link_mode = NULL
",
)
.bind(instance_id)
.bind(option_name)
.bind(variant)
.bind(expected_sha1)
.bind(merge_base)
.bind(source_revision)
.execute(&state.pool)
.await?;
Ok(())
}
pub(in crate::api::instance) async fn finish_checkpoint(
instance_id: &str,
option: SyncedOption,
variant: &str,
mode: LinkMode,
state: &State,
) -> crate::Result<()> {
let option_name = option.as_str();
let link_mode = mode.as_str();
sqlx::query(
"
UPDATE instance_sync_checkpoints
SET status = 'ready', link_mode = ?
WHERE instance_id = ? AND feature = ? AND variant = ?
",
)
.bind(link_mode)
.bind(instance_id)
.bind(option_name)
.bind(variant)
.execute(&state.pool)
.await?;
Ok(())
}
pub(in crate::api::instance) async fn finish_plain_checkpoint(
instance_id: &str,
option: SyncedOption,
variant: &str,
state: &State,
) -> crate::Result<()> {
let option_name = option.as_str();
sqlx::query(
"
UPDATE instance_sync_checkpoints
SET status = 'ready', link_mode = NULL
WHERE instance_id = ? AND feature = ? AND variant = ?
",
)
.bind(instance_id)
.bind(option_name)
.bind(variant)
.execute(&state.pool)
.await?;
Ok(())
}
pub(in crate::api::instance) async fn checkpoint(
instance_id: &str,
option: SyncedOption,
variant: &str,
state: &State,
) -> crate::Result<Option<SyncCheckpoint>> {
let option_name = option.as_str();
let row = sqlx::query(
r#"
SELECT expected_sha1, merge_base, source_revision, status
FROM instance_sync_checkpoints
WHERE instance_id = ? AND feature = ? AND variant = ?
"#,
)
.bind(instance_id)
.bind(option_name)
.bind(variant)
.fetch_optional(&state.pool)
.await?;
use sqlx::Row;
row.map(|row| {
let status: String = row.get("status");
Ok(SyncCheckpoint {
expected_sha1: row.get("expected_sha1"),
merge_base: row.get("merge_base"),
source_revision: row.get("source_revision"),
status: CheckpointStatus::from_str(&status).ok_or_else(|| {
ErrorKind::InputError(format!(
"Unknown sync checkpoint status {status}"
))
})?,
})
})
.transpose()
}
pub(in crate::api::instance) async fn sha1_file(
path: &Path,
) -> crate::Result<String> {
Ok(Sha1::from(io::read(path).await?).digest().to_string())
}
pub(in crate::api::instance) fn sha1_bytes(bytes: &[u8]) -> String {
Sha1::from(bytes).digest().to_string()
}
pub(in crate::api::instance) async fn read_nbt_file(
path: &Path,
) -> crate::Result<NbtCompound> {
let bytes = io::read(path).await?;
let (root, _) = quartz_nbt::io::read_nbt(
&mut Cursor::new(bytes),
quartz_nbt::io::Flavor::Uncompressed,
)?;
Ok(root)
}
pub(in crate::api::instance) fn nbt_to_bytes(
root: &NbtCompound,
) -> crate::Result<Vec<u8>> {
let mut bytes = Vec::new();
quartz_nbt::io::write_nbt(
&mut bytes,
None,
root,
quartz_nbt::io::Flavor::Uncompressed,
)?;
Ok(bytes)
}
pub(in crate::api::instance) fn nbt_from_bytes(
bytes: Vec<u8>,
) -> crate::Result<NbtCompound> {
let (root, _) = quartz_nbt::io::read_nbt(
&mut Cursor::new(bytes),
quartz_nbt::io::Flavor::Uncompressed,
)?;
Ok(root)
}