feat: 跨实例同步移植 WIP 检查点(第1轮+2a+2b+synced_servers)
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
从 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轮),当前编译不通。
This commit is contained in:
1 parent
5a289c3a15
commit
ed61a5942c
43 files changed
+10607
No files matched your search
@@ -0,0 +1,338 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user