# Conflicts:
#	.github/workflows/build-on-tag.yml
#	.github/workflows/sync-cnb-source.yml
This commit is contained in:
AxTps committed 2026-09-25 22:29:40 +08:00
commit 6ce39f3a42
43 files changed
+1477 -716

No files matched your search

+131 -1
View File
@@ -810,11 +810,85 @@ pub async fn prepare_launch(
"StarLight 实例必须登录并联网检查更新后才能启动;离线游玩请使用本地实例",
));
}
synchronize_locked(instance_id).await?;
if crate::process::get_by_instance_id(instance_id)
.await?
.is_empty()
{
synchronize_locked(instance_id).await?;
} else {
check_running_pack(instance_id).await?;
}
}
Ok(guard)
}
async fn check_running_pack(instance_id: &str) -> crate::Result<()> {
let auth = authorization().await?;
let root = crate::instance::get_full_path(instance_id).await?;
let previous: Binding = read_json(&root, BINDING)
.await?
.ok_or_else(|| invalid("请先关闭该实例的所有游戏窗口,再同步整合包"))?;
let remote = request_authorized::<Option<SyncState>>("/sync-state", &auth)
.await?
.ok_or_else(|| {
invalid("管理员尚未指定已发布的默认整合包,请联系服务器管理员")
})?;
if !marker_matches(&previous, &remote) {
return Err(invalid(
"整合包有更新,请先关闭该实例的所有游戏窗口,更新完成后再多开",
));
}
let tagged = request_authorized::<tagged::TaggedManifest>(
&format!("/tagged-mods/{}", previous.publication.release_id),
&auth,
)
.await?;
tagged.validate()?;
if !running_pack_matches(&root, &previous, &tagged).await? {
return Err(invalid(
"整合包 Mod 有变更,请先关闭该实例的所有游戏窗口,更新完成后再多开",
));
}
ensure_session(&auth).await
}
async fn running_pack_matches(
root: &Path,
previous: &Binding,
tagged: &tagged::TaggedManifest,
) -> crate::Result<bool> {
let mut desired = previous.publication.manifest.files.clone();
desired.extend(previous.resolved_external.iter().cloned());
let duplicates = tagged::merge(
root,
&target(root, ".starlight-pack-cache")?,
&mut desired,
&mut BTreeMap::new(),
tagged,
)
.await?;
validate(&desired)?;
let inventory = |files: &[PackFile]| {
files
.iter()
.map(|file| (file.path.clone(), file.sha256.clone()))
.collect::<BTreeMap<_, _>>()
};
if !duplicates.is_empty()
|| inventory(&desired) != inventory(&previous.files)
{
return Ok(false);
}
for file in desired.iter().filter(|file| tagged.includes_content(file)) {
if hash(&target(root, &file.path)?).await?.as_deref()
!= Some(&file.sha256)
{
return Ok(false);
}
}
Ok(true)
}
async fn synchronize_locked(instance_id: &str) -> crate::Result<SyncResult> {
ensure_idle(instance_id).await?;
recover(instance_id).await?;
@@ -1418,6 +1492,62 @@ async fn synchronize_with_progress(
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn running_pack_checks_tag_changes_without_writing_live_files() {
let root = tempfile::tempdir().unwrap();
tokio::fs::create_dir(root.path().join("mods"))
.await
.unwrap();
let path = root.path().join("mods/example.starlight.jar");
tokio::fs::write(&path, b"installed").await.unwrap();
let sha = hash(&path).await.unwrap().unwrap();
let file = serde_json::json!({"path":"mods/example.starlight.jar", "sha256":sha, "size":9,
"modIds":["example"], "force":true});
let previous: Binding = serde_json::from_value(serde_json::json!({
"publication":{"packId":"pack", "releaseId":1, "manifest":{
"schemaVersion":1, "name":"Pack", "version":"1", "format":"multimc",
"runtime":{"gameVersion":"1.21.1", "loader":"vanilla"}, "files":[]
}}, "files":[file], "syncMarker":"pack:1"
})).unwrap();
let manifest = |sha: &str| {
serde_json::from_value::<tagged::TaggedManifest>(serde_json::json!({
"files":[{"modId":"example", "modIds":["example"], "path":"mods/example.starlight.jar", "sha256":sha, "size":9}],
"replaces":[]
})).unwrap()
};
assert!(
running_pack_matches(root.path(), &previous, &manifest(&sha))
.await
.unwrap()
);
assert!(
!running_pack_matches(
root.path(),
&previous,
&manifest(&"a".repeat(64))
)
.await
.unwrap()
);
let empty = serde_json::from_value(
serde_json::json!({"files":[], "replaces":[]}),
)
.unwrap();
assert!(
!running_pack_matches(root.path(), &previous, &empty)
.await
.unwrap()
);
assert_eq!(tokio::fs::read(&path).await.unwrap(), b"installed");
assert!(!root.path().join(JOURNAL).exists());
tokio::fs::write(&path, b"modified").await.unwrap();
assert!(
!running_pack_matches(root.path(), &previous, &manifest(&sha))
.await
.unwrap()
);
}
#[test]
fn sync_marker_requires_an_installed_publication_and_changes_on_default_or_release()
{
@@ -23,6 +23,10 @@ pub(super) struct TaggedManifest {
}
impl TaggedManifest {
pub fn includes_content(&self, file: &PackFile) -> bool {
self.files.iter().any(|tagged| tagged.sha256 == file.sha256)
}
pub fn contains(&self, path: &str) -> bool {
self.files.iter().any(|file| file.path == path)
}
@@ -51,8 +51,7 @@ pub async fn import_generic(
let info = detect_instance_info(&layout.json_source, overrides).await?;
register_instance(instance_id, &layout.name, &info).await?;
copy_instance_files(instance_id, &layout, reporter, details, symlink)
.await
copy_instance_files(instance_id, &layout, reporter, details, symlink).await
}
/// The resolved source layout for a generic import.
@@ -589,8 +589,8 @@ fn try_extract_version_from_needle(
) -> Option<String> {
let pos = content.find(needle)?;
let after = &content[pos + needle.len()..];
let end = after
.find(&['"', ',', '\n', '}', ']', '[', ':', ' '] as &[char])?;
let end =
after.find(&['"', ',', '\n', '}', ']', '[', ':', ' '] as &[char])?;
let ver = &after[..end];
if let Some(ch) = split_at
&& let Some(pos) = ver.rfind(ch)
@@ -609,7 +609,11 @@ fn sanitize_loader_version(raw: &str) -> String {
// Cut at the first character that cannot appear in a loader version id.
let end = trimmed
.find(|ch: char| {
!(ch.is_ascii_alphanumeric() || ch == '.' || ch == '-' || ch == '_' || ch == '+')
!(ch.is_ascii_alphanumeric()
|| ch == '.'
|| ch == '-'
|| ch == '_'
|| ch == '+')
})
.unwrap_or(trimmed.len());
trimmed[..end].trim().to_string()
+8 -3
View File
@@ -1234,10 +1234,13 @@ fn is_other_version_entry(rel: &Path, keep: &Path) -> bool {
let mut keep_components = keep.components();
// Both must start with the literal `versions` component.
if rel_components.next().map(|c| c.as_os_str()) != Some("versions".as_ref()) {
if rel_components.next().map(|c| c.as_os_str()) != Some("versions".as_ref())
{
return false;
}
if keep_components.next().map(|c| c.as_os_str()) != Some("versions".as_ref()) {
if keep_components.next().map(|c| c.as_os_str())
!= Some("versions".as_ref())
{
return false;
}
@@ -1253,7 +1256,9 @@ fn is_other_version_entry(rel: &Path, keep: &Path) -> bool {
let rel_is_inside_version_dir = rel_components.next().is_some();
match (rel_version, keep_version) {
(Some(rel_v), Some(keep_v)) if rel_is_inside_version_dir => rel_v != keep_v,
(Some(rel_v), Some(keep_v)) if rel_is_inside_version_dir => {
rel_v != keep_v
}
// A file directly under `versions/`, shared metadata: keep it.
_ => false,
}