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轮),当前编译不通。
339 lines
8.8 KiB
Rust
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)
|
|
}
|