mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
feat: instance sharing thru shared-instances service (#6569)
* feat: implement instance share page + search_users backend call * feat: invite players modal * feat: use tanstack queries for friends sync across app pages * feat: base shared instances implementation * fix: admon style * feat: impl instance admonitions like server panel * fix: impl get + del usage * feat: support modpack links * feat: invite notif accepting * fix: lint + fmt * feat: impl install to play * feat: impl usage of UpdateToPlayModal * feat: warnings on deleting/disabling shared-instance version content * fix: send instance name * feat: align with backend * feat: shared instances qa * feat: wrong account protection * feat: qa * fix: smartly apply updates * fix: install bug * fix: 401/404 differentiation * fix: fmt+prepr * feat: qa * feat: qa * fix: signing out messes up revoke/deleted checks * feat: qa * fix: fmt + lint * feat: lock content if part of shared instance * fix: lint * [do not merge] feat: rough invite links impl temp (#6666) * fix: wrong cmd * feat: invite page * fix: server-manager DTO mismatch * fix: drop anonymous invite link acceptance * refactor: structured shared-instance unavailable errors * refactor: centralise error presentations * refactor: dedupe shared instance diff detection * fix: logging in reqwests * refactor: move app.vue shared instances into handler * refactor: break up Share.vue * refactor: split up shared instances state outside of instance index * refactor: dedicated shared instances install/update modals + split up page * refactor: centralized managed content * refactor: split up install shared to own runner + shared.rs split up * refactor: dedupe sql for instance metadata enrichmnt * refactor: friends composable + dedupe friends logic across usages * chore: reduced unused code * fix: align with backend * fix: lint * fix: file sha changes * fix: invite links not working due to icon signed * feat: qa * feat: reporting frontend dummy * fix: try use header * remove: file hash field * fix: pin box * feat: malware warning for shared instances * fix: cache rule * feat: config files syncing * feat: disable config sharing * fix: header * fix: use mark ready * fix: dont cause push update for configs * fix: lint * feat: sharing page in settings * feat: move config + change flow * fix: qa * fix: lint prepr * feat: proxy file upload thru shared instances backend * fix: use collapisible * fix: push config * fix: config * feat: swap out sign in modal for new one * fix: report flow * fix: exclude configs.zip from external warnings * fix: nuxi init * fix: config bundle downloading * fix: error notif * fix: polling * fix: qa * fix: lint + prepr * feat: shared instances moderation frontend + hook up report flow * fix: report copy * fix: lint * fix: lint * fix: modrinth ids being undefined * feat: instance quarantining * fix: prepr + fmt * fix: quarantined -> locked terminology * fix: missing endpoint impls + fmt * fix: missing api in build.rs * fix: share tab jittery * fix: fmt *PT bug * fix: invites count as users even if pending * fix: prepr * fix: invite page owner in users list * fix: lint * fix: qa * fix: lint * fix: members stale not clearing * fix: invite use joined_at field * fix: lint * fix: qa --------- Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
This commit is contained in:
@@ -240,6 +240,11 @@ impl FriendsSocket {
|
||||
}
|
||||
|
||||
async fn handle_notification(notification: Value) -> crate::Result<()> {
|
||||
tracing::info!(
|
||||
notification = %notification,
|
||||
"Received websocket notification payload"
|
||||
);
|
||||
|
||||
if notification
|
||||
.get("body")
|
||||
.and_then(|body| body.get("type"))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
use super::instances::ContentSourceKind;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InstanceInstallStage {
|
||||
@@ -122,6 +124,7 @@ pub struct ContentFile {
|
||||
pub metadata: Option<FileMetadata>,
|
||||
pub update_version_id: Option<String>,
|
||||
pub project_type: ProjectType,
|
||||
pub source_kind: Option<ContentSourceKind>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -130,7 +133,7 @@ pub struct FileMetadata {
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProjectType {
|
||||
Mod,
|
||||
@@ -186,6 +189,18 @@ impl ProjectType {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"mod" | "mods" => Some(ProjectType::Mod),
|
||||
"datapack" | "datapacks" => Some(ProjectType::DataPack),
|
||||
"resourcepack" | "resourcepacks" => Some(ProjectType::ResourcePack),
|
||||
"shader" | "shaderpack" | "shaderpacks" => {
|
||||
Some(ProjectType::ShaderPack)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_folder(&self) -> &'static str {
|
||||
match self {
|
||||
ProjectType::Mod => "mods",
|
||||
|
||||
@@ -89,6 +89,9 @@ fn is_scannable_project_file(
|
||||
ProjectType::Mod => extension.eq_ignore_ascii_case("jar"),
|
||||
ProjectType::DataPack
|
||||
| ProjectType::ResourcePack
|
||||
| ProjectType::ShaderPack => extension.eq_ignore_ascii_case("zip"),
|
||||
| ProjectType::ShaderPack => {
|
||||
extension.eq_ignore_ascii_case("zip")
|
||||
|| extension.eq_ignore_ascii_case("jar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +374,56 @@ where
|
||||
rows.into_iter().map(TryInto::try_into).collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_content_set_remote_ref(
|
||||
remote_ref: &ContentSetRemoteRef,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let content_set_id = remote_ref.content_set_id.as_str();
|
||||
let ref_type = remote_ref.ref_type.as_str();
|
||||
let ref_id = remote_ref.ref_id.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO instance_content_set_remote_refs (
|
||||
content_set_id,
|
||||
ref_type,
|
||||
ref_id
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (content_set_id, ref_type) DO UPDATE SET
|
||||
ref_id = excluded.ref_id
|
||||
",
|
||||
content_set_id,
|
||||
ref_type,
|
||||
ref_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_content_set_remote_ref(
|
||||
content_set_id: &str,
|
||||
ref_type: ContentSetRemoteRefType,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let ref_type = ref_type.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM instance_content_set_remote_refs
|
||||
WHERE content_set_id = ? AND ref_type = ?
|
||||
",
|
||||
content_set_id,
|
||||
ref_type,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_content_set_sync_state<'e, E>(
|
||||
content_set_id: &str,
|
||||
exec: E,
|
||||
@@ -396,6 +446,66 @@ where
|
||||
row.map(TryInto::try_into).transpose()
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_content_set_sync_state(
|
||||
sync_state: &ContentSetSyncState,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let content_set_id = sync_state.content_set_id.as_str();
|
||||
let provider = sync_state.provider.as_str();
|
||||
let applied_update_id = sync_state.applied_update_id.as_deref();
|
||||
let latest_available_update_id =
|
||||
sync_state.latest_available_update_id.as_deref();
|
||||
let checked_at = sync_state.checked_at.map(|value| value.timestamp());
|
||||
let status = sync_state.status.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO instance_content_set_sync_state (
|
||||
content_set_id,
|
||||
provider,
|
||||
applied_update_id,
|
||||
latest_available_update_id,
|
||||
checked_at,
|
||||
status
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (content_set_id) DO UPDATE SET
|
||||
provider = excluded.provider,
|
||||
applied_update_id = excluded.applied_update_id,
|
||||
latest_available_update_id = excluded.latest_available_update_id,
|
||||
checked_at = excluded.checked_at,
|
||||
status = excluded.status
|
||||
",
|
||||
content_set_id,
|
||||
provider,
|
||||
applied_update_id,
|
||||
latest_available_update_id,
|
||||
checked_at,
|
||||
status,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_content_set_sync_state(
|
||||
content_set_id: &str,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM instance_content_set_sync_state
|
||||
WHERE content_set_id = ?
|
||||
",
|
||||
content_set_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_instance_files<'e, E>(
|
||||
instance_id: &str,
|
||||
exec: E,
|
||||
@@ -523,6 +633,100 @@ pub(crate) async fn upsert_instance_file(
|
||||
row.try_into()
|
||||
}
|
||||
|
||||
async fn insert_content_entry(
|
||||
entry: &ContentEntry,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let id = entry.id.as_str();
|
||||
let instance_id = entry.instance_id.as_str();
|
||||
let content_set_id = entry.content_set_id.as_str();
|
||||
let file_id = entry.file_id.as_deref();
|
||||
let project_type = entry.project_type.get_name();
|
||||
let project_id = entry.project_id.as_deref();
|
||||
let version_id = entry.version_id.as_deref();
|
||||
let source_kind = entry.source_kind.as_str();
|
||||
let server_requirement = entry.server_requirement.as_str();
|
||||
let client_requirement = entry.client_requirement.as_str();
|
||||
let enabled = i64::from(entry.enabled);
|
||||
let added_at = entry.added_at.timestamp();
|
||||
let modified_at = entry.modified_at.timestamp();
|
||||
|
||||
sqlx::query(
|
||||
"
|
||||
INSERT INTO instance_content_entries (
|
||||
id,
|
||||
instance_id,
|
||||
content_set_id,
|
||||
file_id,
|
||||
project_type,
|
||||
project_id,
|
||||
version_id,
|
||||
source_kind,
|
||||
server_requirement,
|
||||
client_requirement,
|
||||
enabled,
|
||||
added_at,
|
||||
modified_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(instance_id)
|
||||
.bind(content_set_id)
|
||||
.bind(file_id)
|
||||
.bind(project_type)
|
||||
.bind(project_id)
|
||||
.bind(version_id)
|
||||
.bind(source_kind)
|
||||
.bind(server_requirement)
|
||||
.bind(client_requirement)
|
||||
.bind(enabled)
|
||||
.bind(added_at)
|
||||
.bind(modified_at)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn restore_instance_content_snapshot(
|
||||
instance_id: &str,
|
||||
files: &[InstanceFile],
|
||||
entries: &[ContentEntry],
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
let mut tx = pool.begin().await?;
|
||||
sqlx::query(
|
||||
"
|
||||
DELETE FROM instance_content_entries
|
||||
WHERE instance_id = ?
|
||||
",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"
|
||||
DELETE FROM instance_files
|
||||
WHERE instance_id = ?
|
||||
",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for file in files {
|
||||
upsert_instance_file(file, &mut tx).await?;
|
||||
}
|
||||
for entry in entries {
|
||||
insert_content_entry(entry, &mut tx).await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_content_entries<'e, E>(
|
||||
content_set_id: &str,
|
||||
exec: E,
|
||||
@@ -1091,16 +1295,12 @@ pub(crate) async fn upsert_content_update_check(
|
||||
}
|
||||
|
||||
fn project_type_from_str(value: &str) -> crate::Result<ProjectType> {
|
||||
match value {
|
||||
"mod" => Ok(ProjectType::Mod),
|
||||
"datapack" => Ok(ProjectType::DataPack),
|
||||
"resourcepack" => Ok(ProjectType::ResourcePack),
|
||||
"shader" | "shaderpack" => Ok(ProjectType::ShaderPack),
|
||||
other => Err(crate::ErrorKind::InputError(format!(
|
||||
"Unknown content project type {other}"
|
||||
ProjectType::from_name(value).ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown content project type {value}"
|
||||
))
|
||||
.into()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn timestamp(value: i64) -> DateTime<Utc> {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::state::instances::{
|
||||
ContentSet, ContentSetStatus, ContentSourceKind, Instance,
|
||||
InstanceLaunchContext, InstanceLaunchOverrides,
|
||||
InstanceLaunchOverridesData, InstanceLink, playtime_to_storage,
|
||||
ContentSet, ContentSetStatus, ContentSetSyncStatus, ContentSourceKind,
|
||||
Instance, InstanceLaunchContext, InstanceLaunchOverrides,
|
||||
InstanceLaunchOverridesData, InstanceLink, SharedInstanceAttachment,
|
||||
SharedInstanceRole, playtime_to_storage,
|
||||
};
|
||||
use crate::state::{
|
||||
InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel,
|
||||
@@ -73,6 +74,9 @@ pub(crate) struct InstanceLinkRow {
|
||||
pub hosting_instance_ids: Option<String>,
|
||||
pub hosting_active_instance_id: Option<String>,
|
||||
pub shared_instance_id: Option<String>,
|
||||
pub shared_instance_role: Option<String>,
|
||||
pub shared_instance_manager_id: Option<String>,
|
||||
pub shared_instance_linked_user_id: Option<String>,
|
||||
pub imported_name: Option<String>,
|
||||
pub imported_version_number: Option<String>,
|
||||
pub imported_filename: Option<String>,
|
||||
@@ -137,10 +141,8 @@ impl TryFrom<InstanceLinkRow> for InstanceLink {
|
||||
filename: row.imported_filename,
|
||||
}),
|
||||
"shared_instance" => Ok(Self::SharedInstance {
|
||||
shared_instance_id: parse_uuid(
|
||||
row.shared_instance_id,
|
||||
"shared_instance_id",
|
||||
)?,
|
||||
modpack_project_id: row.modrinth_project_id,
|
||||
modpack_version_id: row.modrinth_version_id,
|
||||
}),
|
||||
other => Err(crate::ErrorKind::InputError(format!(
|
||||
"Unknown instance link kind {other}"
|
||||
@@ -161,6 +163,7 @@ pub(crate) struct InstanceMetadataRecord {
|
||||
pub instance: Instance,
|
||||
pub applied_content_set: ContentSet,
|
||||
pub link: InstanceLink,
|
||||
pub shared_instance: Option<SharedInstanceAttachment>,
|
||||
pub groups: Vec<String>,
|
||||
pub launch_overrides: InstanceLaunchOverrides,
|
||||
}
|
||||
@@ -207,6 +210,14 @@ struct InstanceMetadataRow {
|
||||
hosting_instance_ids: Option<String>,
|
||||
hosting_active_instance_id: Option<String>,
|
||||
shared_instance_id: Option<String>,
|
||||
shared_instance_role: Option<String>,
|
||||
shared_instance_manager_id: Option<String>,
|
||||
shared_instance_server_manager_name: Option<String>,
|
||||
shared_instance_server_manager_icon_url: Option<String>,
|
||||
shared_instance_linked_user_id: Option<String>,
|
||||
shared_sync_applied_update_id: Option<String>,
|
||||
shared_sync_latest_available_update_id: Option<String>,
|
||||
shared_sync_status: Option<String>,
|
||||
imported_name: Option<String>,
|
||||
imported_version_number: Option<String>,
|
||||
imported_filename: Option<String>,
|
||||
@@ -300,12 +311,28 @@ impl InstanceMetadataRow {
|
||||
hosting_server_id: self.hosting_server_id,
|
||||
hosting_instance_ids: self.hosting_instance_ids,
|
||||
hosting_active_instance_id: self.hosting_active_instance_id,
|
||||
shared_instance_id: self.shared_instance_id,
|
||||
shared_instance_id: self.shared_instance_id.clone(),
|
||||
shared_instance_role: self.shared_instance_role.clone(),
|
||||
shared_instance_manager_id: self.shared_instance_manager_id.clone(),
|
||||
shared_instance_linked_user_id: self
|
||||
.shared_instance_linked_user_id
|
||||
.clone(),
|
||||
imported_name: self.imported_name,
|
||||
imported_version_number: self.imported_version_number,
|
||||
imported_filename: self.imported_filename,
|
||||
}
|
||||
.try_into()?;
|
||||
let shared_instance = shared_instance_attachment(
|
||||
self.shared_instance_id,
|
||||
self.shared_instance_role,
|
||||
self.shared_instance_manager_id,
|
||||
self.shared_instance_server_manager_name,
|
||||
self.shared_instance_server_manager_icon_url,
|
||||
self.shared_instance_linked_user_id,
|
||||
self.shared_sync_status,
|
||||
self.shared_sync_applied_update_id,
|
||||
self.shared_sync_latest_available_update_id,
|
||||
)?;
|
||||
let groups = parse_groups(self.groups)?;
|
||||
let launch_overrides =
|
||||
launch_overrides_from_json(instance_id, self.launch_overrides)?;
|
||||
@@ -314,6 +341,7 @@ impl InstanceMetadataRow {
|
||||
instance,
|
||||
applied_content_set,
|
||||
link,
|
||||
shared_instance,
|
||||
groups,
|
||||
launch_overrides,
|
||||
})
|
||||
@@ -418,75 +446,173 @@ where
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub(crate) async fn is_instance_quarantined<'e, E>(
|
||||
instance_id: &str,
|
||||
exec: E,
|
||||
) -> crate::Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Sqlite>,
|
||||
{
|
||||
let quarantined = sqlx::query_scalar::<_, bool>(
|
||||
"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM instance_quarantines
|
||||
WHERE instance_id = ?
|
||||
)
|
||||
",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(quarantined)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_quarantined_instance_ids<'e, E>(
|
||||
exec: E,
|
||||
) -> crate::Result<std::collections::HashSet<String>>
|
||||
where
|
||||
E: Executor<'e, Database = Sqlite>,
|
||||
{
|
||||
let instance_ids = sqlx::query_scalar::<_, String>(
|
||||
"
|
||||
SELECT instance_id
|
||||
FROM instance_quarantines
|
||||
",
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(instance_ids.into_iter().collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_instance_quarantined(
|
||||
instance_id: &str,
|
||||
quarantined: bool,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
if quarantined {
|
||||
sqlx::query(
|
||||
"
|
||||
INSERT INTO instance_quarantines (instance_id)
|
||||
VALUES (?)
|
||||
ON CONFLICT (instance_id) DO NOTHING
|
||||
",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query(
|
||||
"
|
||||
DELETE FROM instance_quarantines
|
||||
WHERE instance_id = ?
|
||||
",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! query_instance_metadata {
|
||||
(
|
||||
$prefix:literal,
|
||||
$from:literal,
|
||||
$suffix:literal,
|
||||
$arg:expr $(,)?
|
||||
) => {
|
||||
sqlx::query_as!(
|
||||
InstanceMetadataRow,
|
||||
$prefix
|
||||
+ r#"
|
||||
SELECT
|
||||
i.id AS "id!: String",
|
||||
i.path AS "path!: String",
|
||||
i.applied_content_set_id AS "applied_content_set_id?: String",
|
||||
i.install_stage AS "install_stage!: String",
|
||||
i.launcher_feature_version AS "launcher_feature_version!: String",
|
||||
i.update_channel AS "update_channel!: String",
|
||||
i.name AS "name!: String",
|
||||
i.icon_path AS "icon_path?: String",
|
||||
i.created AS "created!: i64",
|
||||
i.modified AS "modified!: i64",
|
||||
i.last_played AS "last_played?: i64",
|
||||
i.submitted_time_played AS "submitted_time_played!: i64",
|
||||
i.recent_time_played AS "recent_time_played!: i64",
|
||||
cs.id AS "content_set_id?: String",
|
||||
cs.instance_id AS "content_set_instance_id?: String",
|
||||
cs.name AS "content_set_name?: String",
|
||||
cs.source_kind AS "content_set_source_kind?: String",
|
||||
cs.status AS "content_set_status?: String",
|
||||
cs.game_version AS "content_set_game_version?: String",
|
||||
cs.protocol_version AS "content_set_protocol_version?: i64",
|
||||
cs.loader AS "content_set_loader?: String",
|
||||
cs.loader_version AS "content_set_loader_version?: String",
|
||||
cs.created AS "content_set_created?: i64",
|
||||
cs.modified AS "content_set_modified?: i64",
|
||||
COALESCE(link.link_kind, 'unmanaged') AS "link_kind!: String",
|
||||
link.modrinth_project_id AS "modrinth_project_id?: String",
|
||||
link.modrinth_version_id AS "modrinth_version_id?: String",
|
||||
link.server_project_id AS "server_project_id?: String",
|
||||
link.content_project_id AS "content_project_id?: String",
|
||||
link.content_version_id AS "content_version_id?: String",
|
||||
link.hosting_server_id AS "hosting_server_id?: String",
|
||||
json(link.hosting_instance_ids) AS "hosting_instance_ids?: String",
|
||||
link.hosting_active_instance_id AS "hosting_active_instance_id?: String",
|
||||
link.shared_instance_id AS "shared_instance_id?: String",
|
||||
link.shared_instance_role AS "shared_instance_role?: String",
|
||||
link.shared_instance_manager_id AS "shared_instance_manager_id?: String",
|
||||
link.shared_instance_server_manager_name AS "shared_instance_server_manager_name?: String",
|
||||
link.shared_instance_server_manager_icon_url AS "shared_instance_server_manager_icon_url?: String",
|
||||
link.shared_instance_linked_user_id AS "shared_instance_linked_user_id?: String",
|
||||
sync.applied_update_id AS "shared_sync_applied_update_id?: String",
|
||||
sync.latest_available_update_id AS "shared_sync_latest_available_update_id?: String",
|
||||
sync.status AS "shared_sync_status?: String",
|
||||
link.imported_name AS "imported_name?: String",
|
||||
link.imported_version_number AS "imported_version_number?: String",
|
||||
link.imported_filename AS "imported_filename?: String",
|
||||
COALESCE((
|
||||
SELECT json_group_array(group_name)
|
||||
FROM (
|
||||
SELECT group_name
|
||||
FROM instance_groups
|
||||
WHERE instance_id = i.id
|
||||
ORDER BY group_name
|
||||
)
|
||||
), '[]') AS "groups!: String",
|
||||
json(overrides.overrides) AS "launch_overrides?: String"
|
||||
"#
|
||||
+ $from
|
||||
+ r#"
|
||||
LEFT JOIN instance_content_sets cs
|
||||
ON cs.id = i.applied_content_set_id
|
||||
AND cs.instance_id = i.id
|
||||
LEFT JOIN instance_links link
|
||||
ON link.instance_id = i.id
|
||||
LEFT JOIN instance_content_set_sync_state sync
|
||||
ON sync.content_set_id = cs.id
|
||||
AND sync.provider = 'shared_instance'
|
||||
LEFT JOIN instance_launch_overrides overrides
|
||||
ON overrides.instance_id = i.id
|
||||
"#
|
||||
+ $suffix,
|
||||
$arg,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn get_instance_metadata_by_id(
|
||||
id: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Option<InstanceMetadataRecord>> {
|
||||
let row = sqlx::query_as!(
|
||||
InstanceMetadataRow,
|
||||
r#"
|
||||
SELECT
|
||||
i.id AS "id!: String",
|
||||
i.path AS "path!: String",
|
||||
i.applied_content_set_id AS "applied_content_set_id?: String",
|
||||
i.install_stage AS "install_stage!: String",
|
||||
i.launcher_feature_version AS "launcher_feature_version!: String",
|
||||
i.update_channel AS "update_channel!: String",
|
||||
i.name AS "name!: String",
|
||||
i.icon_path AS "icon_path?: String",
|
||||
i.created AS "created!: i64",
|
||||
i.modified AS "modified!: i64",
|
||||
i.last_played AS "last_played?: i64",
|
||||
i.submitted_time_played AS "submitted_time_played!: i64",
|
||||
i.recent_time_played AS "recent_time_played!: i64",
|
||||
cs.id AS "content_set_id?: String",
|
||||
cs.instance_id AS "content_set_instance_id?: String",
|
||||
cs.name AS "content_set_name?: String",
|
||||
cs.source_kind AS "content_set_source_kind?: String",
|
||||
cs.status AS "content_set_status?: String",
|
||||
cs.game_version AS "content_set_game_version?: String",
|
||||
cs.protocol_version AS "content_set_protocol_version?: i64",
|
||||
cs.loader AS "content_set_loader?: String",
|
||||
cs.loader_version AS "content_set_loader_version?: String",
|
||||
cs.created AS "content_set_created?: i64",
|
||||
cs.modified AS "content_set_modified?: i64",
|
||||
COALESCE(link.link_kind, 'unmanaged') AS "link_kind!: String",
|
||||
link.modrinth_project_id AS "modrinth_project_id?: String",
|
||||
link.modrinth_version_id AS "modrinth_version_id?: String",
|
||||
link.server_project_id AS "server_project_id?: String",
|
||||
link.content_project_id AS "content_project_id?: String",
|
||||
link.content_version_id AS "content_version_id?: String",
|
||||
link.hosting_server_id AS "hosting_server_id?: String",
|
||||
json(link.hosting_instance_ids) AS "hosting_instance_ids?: String",
|
||||
link.hosting_active_instance_id AS "hosting_active_instance_id?: String",
|
||||
link.shared_instance_id AS "shared_instance_id?: String",
|
||||
link.imported_name AS "imported_name?: String",
|
||||
link.imported_version_number AS "imported_version_number?: String",
|
||||
link.imported_filename AS "imported_filename?: String",
|
||||
COALESCE((
|
||||
SELECT json_group_array(group_name)
|
||||
FROM (
|
||||
SELECT group_name
|
||||
FROM instance_groups
|
||||
WHERE instance_id = i.id
|
||||
ORDER BY group_name
|
||||
)
|
||||
), '[]') AS "groups!: String",
|
||||
json(overrides.overrides) AS "launch_overrides?: String"
|
||||
FROM instances i
|
||||
LEFT JOIN instance_content_sets cs
|
||||
ON cs.id = i.applied_content_set_id
|
||||
AND cs.instance_id = i.id
|
||||
LEFT JOIN instance_links link
|
||||
ON link.instance_id = i.id
|
||||
LEFT JOIN instance_launch_overrides overrides
|
||||
ON overrides.instance_id = i.id
|
||||
WHERE i.id = ?
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
let row =
|
||||
query_instance_metadata!("", "FROM instances i", "WHERE i.id = ?", id,)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
row.map(InstanceMetadataRow::into_record).transpose()
|
||||
}
|
||||
@@ -500,73 +626,19 @@ pub(crate) async fn get_instance_metadata_many(
|
||||
}
|
||||
|
||||
let ids_json = serde_json::to_string(ids)?;
|
||||
let rows = sqlx::query_as!(
|
||||
InstanceMetadataRow,
|
||||
let rows = query_instance_metadata!(
|
||||
r#"
|
||||
WITH requested AS (
|
||||
SELECT value AS id, key AS ord
|
||||
FROM json_each(?)
|
||||
)
|
||||
SELECT
|
||||
i.id AS "id!: String",
|
||||
i.path AS "path!: String",
|
||||
i.applied_content_set_id AS "applied_content_set_id?: String",
|
||||
i.install_stage AS "install_stage!: String",
|
||||
i.launcher_feature_version AS "launcher_feature_version!: String",
|
||||
i.update_channel AS "update_channel!: String",
|
||||
i.name AS "name!: String",
|
||||
i.icon_path AS "icon_path?: String",
|
||||
i.created AS "created!: i64",
|
||||
i.modified AS "modified!: i64",
|
||||
i.last_played AS "last_played?: i64",
|
||||
i.submitted_time_played AS "submitted_time_played!: i64",
|
||||
i.recent_time_played AS "recent_time_played!: i64",
|
||||
cs.id AS "content_set_id?: String",
|
||||
cs.instance_id AS "content_set_instance_id?: String",
|
||||
cs.name AS "content_set_name?: String",
|
||||
cs.source_kind AS "content_set_source_kind?: String",
|
||||
cs.status AS "content_set_status?: String",
|
||||
cs.game_version AS "content_set_game_version?: String",
|
||||
cs.protocol_version AS "content_set_protocol_version?: i64",
|
||||
cs.loader AS "content_set_loader?: String",
|
||||
cs.loader_version AS "content_set_loader_version?: String",
|
||||
cs.created AS "content_set_created?: i64",
|
||||
cs.modified AS "content_set_modified?: i64",
|
||||
COALESCE(link.link_kind, 'unmanaged') AS "link_kind!: String",
|
||||
link.modrinth_project_id AS "modrinth_project_id?: String",
|
||||
link.modrinth_version_id AS "modrinth_version_id?: String",
|
||||
link.server_project_id AS "server_project_id?: String",
|
||||
link.content_project_id AS "content_project_id?: String",
|
||||
link.content_version_id AS "content_version_id?: String",
|
||||
link.hosting_server_id AS "hosting_server_id?: String",
|
||||
json(link.hosting_instance_ids) AS "hosting_instance_ids?: String",
|
||||
link.hosting_active_instance_id AS "hosting_active_instance_id?: String",
|
||||
link.shared_instance_id AS "shared_instance_id?: String",
|
||||
link.imported_name AS "imported_name?: String",
|
||||
link.imported_version_number AS "imported_version_number?: String",
|
||||
link.imported_filename AS "imported_filename?: String",
|
||||
COALESCE((
|
||||
SELECT json_group_array(group_name)
|
||||
FROM (
|
||||
SELECT group_name
|
||||
FROM instance_groups
|
||||
WHERE instance_id = i.id
|
||||
ORDER BY group_name
|
||||
)
|
||||
), '[]') AS "groups!: String",
|
||||
json(overrides.overrides) AS "launch_overrides?: String"
|
||||
"#,
|
||||
r#"
|
||||
FROM requested
|
||||
INNER JOIN instances i
|
||||
ON i.id = requested.id
|
||||
LEFT JOIN instance_content_sets cs
|
||||
ON cs.id = i.applied_content_set_id
|
||||
AND cs.instance_id = i.id
|
||||
LEFT JOIN instance_links link
|
||||
ON link.instance_id = i.id
|
||||
LEFT JOIN instance_launch_overrides overrides
|
||||
ON overrides.instance_id = i.id
|
||||
ORDER BY requested.ord
|
||||
"#,
|
||||
"ORDER BY requested.ord",
|
||||
ids_json,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@@ -580,69 +652,10 @@ pub(crate) async fn get_instance_metadata_many(
|
||||
pub(crate) async fn list_instance_metadata(
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Vec<InstanceMetadataRecord>> {
|
||||
let rows = sqlx::query_as!(
|
||||
InstanceMetadataRow,
|
||||
r#"
|
||||
SELECT
|
||||
i.id AS "id!: String",
|
||||
i.path AS "path!: String",
|
||||
i.applied_content_set_id AS "applied_content_set_id?: String",
|
||||
i.install_stage AS "install_stage!: String",
|
||||
i.launcher_feature_version AS "launcher_feature_version!: String",
|
||||
i.update_channel AS "update_channel!: String",
|
||||
i.name AS "name!: String",
|
||||
i.icon_path AS "icon_path?: String",
|
||||
i.created AS "created!: i64",
|
||||
i.modified AS "modified!: i64",
|
||||
i.last_played AS "last_played?: i64",
|
||||
i.submitted_time_played AS "submitted_time_played!: i64",
|
||||
i.recent_time_played AS "recent_time_played!: i64",
|
||||
cs.id AS "content_set_id?: String",
|
||||
cs.instance_id AS "content_set_instance_id?: String",
|
||||
cs.name AS "content_set_name?: String",
|
||||
cs.source_kind AS "content_set_source_kind?: String",
|
||||
cs.status AS "content_set_status?: String",
|
||||
cs.game_version AS "content_set_game_version?: String",
|
||||
cs.protocol_version AS "content_set_protocol_version?: i64",
|
||||
cs.loader AS "content_set_loader?: String",
|
||||
cs.loader_version AS "content_set_loader_version?: String",
|
||||
cs.created AS "content_set_created?: i64",
|
||||
cs.modified AS "content_set_modified?: i64",
|
||||
COALESCE(link.link_kind, 'unmanaged') AS "link_kind!: String",
|
||||
link.modrinth_project_id AS "modrinth_project_id?: String",
|
||||
link.modrinth_version_id AS "modrinth_version_id?: String",
|
||||
link.server_project_id AS "server_project_id?: String",
|
||||
link.content_project_id AS "content_project_id?: String",
|
||||
link.content_version_id AS "content_version_id?: String",
|
||||
link.hosting_server_id AS "hosting_server_id?: String",
|
||||
json(link.hosting_instance_ids) AS "hosting_instance_ids?: String",
|
||||
link.hosting_active_instance_id AS "hosting_active_instance_id?: String",
|
||||
link.shared_instance_id AS "shared_instance_id?: String",
|
||||
link.imported_name AS "imported_name?: String",
|
||||
link.imported_version_number AS "imported_version_number?: String",
|
||||
link.imported_filename AS "imported_filename?: String",
|
||||
COALESCE((
|
||||
SELECT json_group_array(group_name)
|
||||
FROM (
|
||||
SELECT group_name
|
||||
FROM instance_groups
|
||||
WHERE instance_id = i.id
|
||||
ORDER BY group_name
|
||||
)
|
||||
), '[]') AS "groups!: String",
|
||||
json(overrides.overrides) AS "launch_overrides?: String"
|
||||
FROM instances i
|
||||
LEFT JOIN instance_content_sets cs
|
||||
ON cs.id = i.applied_content_set_id
|
||||
AND cs.instance_id = i.id
|
||||
LEFT JOIN instance_links link
|
||||
ON link.instance_id = i.id
|
||||
LEFT JOIN instance_launch_overrides overrides
|
||||
ON overrides.instance_id = i.id
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
let rows =
|
||||
query_instance_metadata!("", "FROM instances i", "WHERE 1 = ?", 1_i64,)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(InstanceMetadataRow::into_record)
|
||||
@@ -653,59 +666,10 @@ pub(crate) async fn get_instance_launch_context(
|
||||
instance_id: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Option<InstanceLaunchContext>> {
|
||||
let row = sqlx::query_as!(
|
||||
InstanceMetadataRow,
|
||||
r#"
|
||||
SELECT
|
||||
i.id AS "id!: String",
|
||||
i.path AS "path!: String",
|
||||
i.applied_content_set_id AS "applied_content_set_id?: String",
|
||||
i.install_stage AS "install_stage!: String",
|
||||
i.launcher_feature_version AS "launcher_feature_version!: String",
|
||||
i.update_channel AS "update_channel!: String",
|
||||
i.name AS "name!: String",
|
||||
i.icon_path AS "icon_path?: String",
|
||||
i.created AS "created!: i64",
|
||||
i.modified AS "modified!: i64",
|
||||
i.last_played AS "last_played?: i64",
|
||||
i.submitted_time_played AS "submitted_time_played!: i64",
|
||||
i.recent_time_played AS "recent_time_played!: i64",
|
||||
cs.id AS "content_set_id?: String",
|
||||
cs.instance_id AS "content_set_instance_id?: String",
|
||||
cs.name AS "content_set_name?: String",
|
||||
cs.source_kind AS "content_set_source_kind?: String",
|
||||
cs.status AS "content_set_status?: String",
|
||||
cs.game_version AS "content_set_game_version?: String",
|
||||
cs.protocol_version AS "content_set_protocol_version?: i64",
|
||||
cs.loader AS "content_set_loader?: String",
|
||||
cs.loader_version AS "content_set_loader_version?: String",
|
||||
cs.created AS "content_set_created?: i64",
|
||||
cs.modified AS "content_set_modified?: i64",
|
||||
COALESCE(link.link_kind, 'unmanaged') AS "link_kind!: String",
|
||||
link.modrinth_project_id AS "modrinth_project_id?: String",
|
||||
link.modrinth_version_id AS "modrinth_version_id?: String",
|
||||
link.server_project_id AS "server_project_id?: String",
|
||||
link.content_project_id AS "content_project_id?: String",
|
||||
link.content_version_id AS "content_version_id?: String",
|
||||
link.hosting_server_id AS "hosting_server_id?: String",
|
||||
json(link.hosting_instance_ids) AS "hosting_instance_ids?: String",
|
||||
link.hosting_active_instance_id AS "hosting_active_instance_id?: String",
|
||||
link.shared_instance_id AS "shared_instance_id?: String",
|
||||
link.imported_name AS "imported_name?: String",
|
||||
link.imported_version_number AS "imported_version_number?: String",
|
||||
link.imported_filename AS "imported_filename?: String",
|
||||
'[]' AS "groups!: String",
|
||||
json(overrides.overrides) AS "launch_overrides?: String"
|
||||
FROM instances i
|
||||
LEFT JOIN instance_content_sets cs
|
||||
ON cs.id = i.applied_content_set_id
|
||||
AND cs.instance_id = i.id
|
||||
LEFT JOIN instance_links link
|
||||
ON link.instance_id = i.id
|
||||
LEFT JOIN instance_launch_overrides overrides
|
||||
ON overrides.instance_id = i.id
|
||||
WHERE i.id = ?
|
||||
"#,
|
||||
let row = query_instance_metadata!(
|
||||
"",
|
||||
"FROM instances i",
|
||||
"WHERE i.id = ?",
|
||||
instance_id,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
@@ -753,6 +717,9 @@ where
|
||||
json(hosting_instance_ids) AS "hosting_instance_ids?: String",
|
||||
hosting_active_instance_id,
|
||||
shared_instance_id,
|
||||
shared_instance_role,
|
||||
shared_instance_manager_id,
|
||||
shared_instance_linked_user_id,
|
||||
imported_name,
|
||||
imported_version_number,
|
||||
imported_filename
|
||||
@@ -949,7 +916,6 @@ pub(crate) async fn upsert_instance_link(
|
||||
let hosting_instance_ids = columns.hosting_instance_ids.as_deref();
|
||||
let hosting_active_instance_id =
|
||||
columns.hosting_active_instance_id.as_deref();
|
||||
let shared_instance_id = columns.shared_instance_id.as_deref();
|
||||
let imported_name = columns.imported_name.as_deref();
|
||||
let imported_version_number = columns.imported_version_number.as_deref();
|
||||
let imported_filename = columns.imported_filename.as_deref();
|
||||
@@ -967,12 +933,11 @@ pub(crate) async fn upsert_instance_link(
|
||||
hosting_server_id,
|
||||
hosting_instance_ids,
|
||||
hosting_active_instance_id,
|
||||
shared_instance_id,
|
||||
imported_name,
|
||||
imported_version_number,
|
||||
imported_filename
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, jsonb(?), ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, jsonb(?), ?, ?, ?, ?)
|
||||
ON CONFLICT (instance_id) DO UPDATE SET
|
||||
link_kind = excluded.link_kind,
|
||||
modrinth_project_id = excluded.modrinth_project_id,
|
||||
@@ -983,7 +948,6 @@ pub(crate) async fn upsert_instance_link(
|
||||
hosting_server_id = excluded.hosting_server_id,
|
||||
hosting_instance_ids = excluded.hosting_instance_ids,
|
||||
hosting_active_instance_id = excluded.hosting_active_instance_id,
|
||||
shared_instance_id = excluded.shared_instance_id,
|
||||
imported_name = excluded.imported_name,
|
||||
imported_version_number = excluded.imported_version_number,
|
||||
imported_filename = excluded.imported_filename
|
||||
@@ -998,7 +962,6 @@ pub(crate) async fn upsert_instance_link(
|
||||
hosting_server_id,
|
||||
hosting_instance_ids,
|
||||
hosting_active_instance_id,
|
||||
shared_instance_id,
|
||||
imported_name,
|
||||
imported_version_number,
|
||||
imported_filename,
|
||||
@@ -1009,6 +972,64 @@ pub(crate) async fn upsert_instance_link(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_shared_instance_attachment(
|
||||
instance_id: &str,
|
||||
attachment: Option<&SharedInstanceAttachment>,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let shared_instance_id = attachment.map(|value| value.id.to_string());
|
||||
let shared_instance_role =
|
||||
attachment.map(|value| value.role.as_str().to_string());
|
||||
let shared_instance_manager_id =
|
||||
attachment.and_then(|value| value.manager_id.as_deref());
|
||||
let shared_instance_linked_user_id =
|
||||
attachment.and_then(|value| value.linked_user_id.as_deref());
|
||||
let shared_instance_server_manager_name =
|
||||
attachment.and_then(|value| value.server_manager_name.as_deref());
|
||||
let shared_instance_server_manager_icon_url =
|
||||
attachment.and_then(|value| value.server_manager_icon_url.as_deref());
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO instance_links (
|
||||
instance_id,
|
||||
link_kind,
|
||||
shared_instance_id,
|
||||
shared_instance_role,
|
||||
shared_instance_manager_id,
|
||||
shared_instance_server_manager_name,
|
||||
shared_instance_server_manager_icon_url,
|
||||
shared_instance_linked_user_id
|
||||
)
|
||||
VALUES (?, 'unmanaged', ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (instance_id) DO UPDATE SET
|
||||
link_kind = CASE
|
||||
WHEN excluded.shared_instance_id IS NULL
|
||||
AND instance_links.link_kind = 'shared_instance'
|
||||
THEN 'unmanaged'
|
||||
ELSE instance_links.link_kind
|
||||
END,
|
||||
shared_instance_id = excluded.shared_instance_id,
|
||||
shared_instance_role = excluded.shared_instance_role,
|
||||
shared_instance_manager_id = excluded.shared_instance_manager_id,
|
||||
shared_instance_server_manager_name = excluded.shared_instance_server_manager_name,
|
||||
shared_instance_server_manager_icon_url = excluded.shared_instance_server_manager_icon_url,
|
||||
shared_instance_linked_user_id = excluded.shared_instance_linked_user_id
|
||||
",
|
||||
instance_id,
|
||||
shared_instance_id,
|
||||
shared_instance_role,
|
||||
shared_instance_manager_id,
|
||||
shared_instance_server_manager_name,
|
||||
shared_instance_server_manager_icon_url,
|
||||
shared_instance_linked_user_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_instance_groups(
|
||||
instance_id: &str,
|
||||
groups: &[String],
|
||||
@@ -1094,7 +1115,6 @@ struct InstanceLinkColumns {
|
||||
hosting_server_id: Option<String>,
|
||||
hosting_instance_ids: Option<String>,
|
||||
hosting_active_instance_id: Option<String>,
|
||||
shared_instance_id: Option<String>,
|
||||
imported_name: Option<String>,
|
||||
imported_version_number: Option<String>,
|
||||
imported_filename: Option<String>,
|
||||
@@ -1114,7 +1134,6 @@ fn instance_link_columns(
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
shared_instance_id: None,
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
@@ -1132,7 +1151,6 @@ fn instance_link_columns(
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
shared_instance_id: None,
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
@@ -1147,7 +1165,6 @@ fn instance_link_columns(
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
shared_instance_id: None,
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
@@ -1166,7 +1183,6 @@ fn instance_link_columns(
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
shared_instance_id: None,
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
@@ -1186,7 +1202,6 @@ fn instance_link_columns(
|
||||
hosting_instance_ids: Some(serde_json::to_string(instance_ids)?),
|
||||
hosting_active_instance_id: active_instance_id
|
||||
.map(|value| value.to_string()),
|
||||
shared_instance_id: None,
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
@@ -1207,28 +1222,27 @@ fn instance_link_columns(
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
shared_instance_id: None,
|
||||
imported_name: name.clone(),
|
||||
imported_version_number: version_number.clone(),
|
||||
imported_filename: filename.clone(),
|
||||
}),
|
||||
InstanceLink::SharedInstance { shared_instance_id } => {
|
||||
Ok(InstanceLinkColumns {
|
||||
link_kind: "shared_instance",
|
||||
modrinth_project_id: None,
|
||||
modrinth_version_id: None,
|
||||
server_project_id: None,
|
||||
content_project_id: None,
|
||||
content_version_id: None,
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
shared_instance_id: Some(shared_instance_id.to_string()),
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
})
|
||||
}
|
||||
InstanceLink::SharedInstance {
|
||||
modpack_project_id,
|
||||
modpack_version_id,
|
||||
} => Ok(InstanceLinkColumns {
|
||||
link_kind: "shared_instance",
|
||||
modrinth_project_id: modpack_project_id.clone(),
|
||||
modrinth_version_id: modpack_version_id.clone(),
|
||||
server_project_id: None,
|
||||
content_project_id: None,
|
||||
content_version_id: None,
|
||||
hosting_server_id: None,
|
||||
hosting_instance_ids: None,
|
||||
hosting_active_instance_id: None,
|
||||
imported_name: None,
|
||||
imported_version_number: None,
|
||||
imported_filename: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1273,6 +1287,60 @@ fn launch_overrides_from_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn shared_instance_attachment(
|
||||
shared_instance_id: Option<String>,
|
||||
shared_instance_role: Option<String>,
|
||||
shared_instance_manager_id: Option<String>,
|
||||
shared_instance_server_manager_name: Option<String>,
|
||||
shared_instance_server_manager_icon_url: Option<String>,
|
||||
shared_instance_linked_user_id: Option<String>,
|
||||
shared_sync_status: Option<String>,
|
||||
applied_update_id: Option<String>,
|
||||
latest_available_update_id: Option<String>,
|
||||
) -> crate::Result<Option<SharedInstanceAttachment>> {
|
||||
let Some(id) = shared_instance_id else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let role = match shared_instance_role {
|
||||
Some(role) => SharedInstanceRole::from_stored_str(&role)?,
|
||||
None => SharedInstanceRole::Member,
|
||||
};
|
||||
let status = match shared_sync_status {
|
||||
Some(status) => ContentSetSyncStatus::from_str(&status)?,
|
||||
None => ContentSetSyncStatus::Unknown,
|
||||
};
|
||||
|
||||
Ok(Some(SharedInstanceAttachment {
|
||||
id,
|
||||
role,
|
||||
manager_id: shared_instance_manager_id,
|
||||
server_manager_name: shared_instance_server_manager_name,
|
||||
server_manager_icon_url: shared_instance_server_manager_icon_url,
|
||||
linked_user_id: shared_instance_linked_user_id,
|
||||
status,
|
||||
applied_version: optional_i32(applied_update_id, "applied_update_id")?,
|
||||
latest_version: optional_i32(
|
||||
latest_available_update_id,
|
||||
"latest_available_update_id",
|
||||
)?,
|
||||
}))
|
||||
}
|
||||
|
||||
fn optional_i32(
|
||||
value: Option<String>,
|
||||
column: &str,
|
||||
) -> crate::Result<Option<i32>> {
|
||||
value
|
||||
.map(|value| {
|
||||
value.parse().map_err(|err| {
|
||||
crate::ErrorKind::InputError(format!("Invalid {column}: {err}"))
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn parse_uuid(value: Option<String>, column: &str) -> crate::Result<Uuid> {
|
||||
let value = required(value, column)?;
|
||||
|
||||
|
||||
@@ -502,6 +502,13 @@ pub(crate) async fn add_project_bytes(
|
||||
version_id: Option<&str>,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
if !path_util::is_safe_file_name(file_name) {
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Project file {file_name} has an invalid file name"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let _content_lock = state.lock_instance_content(instance_id).await;
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let project_type = match project_type {
|
||||
@@ -561,6 +568,7 @@ pub(crate) async fn add_project_bytes(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
super::mark_shared_instance_stale(instance_id, &state.pool).await?;
|
||||
|
||||
Ok(relative_path)
|
||||
}
|
||||
@@ -608,6 +616,7 @@ pub(crate) async fn record_project_file(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
super::mark_shared_instance_stale(instance_id, &state.pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -711,6 +720,8 @@ pub(crate) async fn toggle_disable_project(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
super::mark_shared_instance_stale(instance_id, &state.pool).await?;
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
@@ -752,9 +763,36 @@ pub(crate) async fn remove_project(
|
||||
tx.commit().await?;
|
||||
}
|
||||
|
||||
super::mark_shared_instance_stale(instance_id, &state.pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn content_source_kind_for_project_path(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<Option<ContentSourceKind>> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let Some(file) = content_rows::get_instance_file_by_relative_path(
|
||||
&scope.instance.id,
|
||||
project_path,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let entries =
|
||||
content_rows::get_content_entries(&scope.content_set_id, &state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(entries.into_iter().find_map(|entry| {
|
||||
(entry.file_id.as_deref() == Some(file.id.as_str()))
|
||||
.then_some(entry.source_kind)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn rename_project_companion_file(
|
||||
instance_id: &str,
|
||||
old_project_path: &str,
|
||||
|
||||
@@ -52,6 +52,7 @@ struct InstalledProject {
|
||||
relative_path: String,
|
||||
project_id: Option<String>,
|
||||
version_id: Option<String>,
|
||||
source_kind: ContentSourceKind,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
@@ -295,8 +296,14 @@ async fn plan_bulk_update(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<BulkUpdatePlan> {
|
||||
let updateable_paths =
|
||||
bulk_updateable_project_paths(instance_id, state).await?;
|
||||
let shared_instance_member =
|
||||
is_shared_instance_member(instance_id, state).await?;
|
||||
let updateable_paths = bulk_updateable_project_paths(
|
||||
instance_id,
|
||||
shared_instance_member,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
if updateable_paths.is_empty() {
|
||||
return Ok(BulkUpdatePlan {
|
||||
project_updates: Vec::new(),
|
||||
@@ -330,6 +337,30 @@ async fn plan_bulk_update(
|
||||
})?;
|
||||
let installed =
|
||||
installed_projects(instance_id, &content_set, state).await?;
|
||||
let updateable_paths = if shared_instance_member {
|
||||
let managed_paths = installed
|
||||
.iter()
|
||||
.filter(|project| project.source_kind.is_shared_instance_managed())
|
||||
.map(|project| project.relative_path.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
updateable_paths
|
||||
.into_iter()
|
||||
.filter(|path| !managed_paths.contains(path))
|
||||
.collect::<HashSet<_>>()
|
||||
} else {
|
||||
updateable_paths
|
||||
};
|
||||
let updates = updates
|
||||
.into_iter()
|
||||
.filter(|update| updateable_paths.contains(&update.relative_path))
|
||||
.collect::<Vec<_>>();
|
||||
if updates.is_empty() {
|
||||
return Ok(BulkUpdatePlan {
|
||||
project_updates: Vec::new(),
|
||||
dependency_additions: Vec::new(),
|
||||
});
|
||||
}
|
||||
let installed_by_project = installed
|
||||
.iter()
|
||||
.filter_map(|project| {
|
||||
@@ -416,6 +447,7 @@ async fn plan_bulk_update(
|
||||
|
||||
async fn bulk_updateable_project_paths(
|
||||
instance_id: &str,
|
||||
shared_instance_member: bool,
|
||||
state: &State,
|
||||
) -> crate::Result<HashSet<String>> {
|
||||
let items = super::list_content::list_content(
|
||||
@@ -426,7 +458,16 @@ async fn bulk_updateable_project_paths(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(items.into_iter().map(|item| item.file_path).collect())
|
||||
Ok(items
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
!shared_instance_member
|
||||
|| !item
|
||||
.source_kind
|
||||
.is_some_and(ContentSourceKind::is_shared_instance_managed)
|
||||
})
|
||||
.map(|item| item.file_path)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn installed_projects(
|
||||
@@ -471,10 +512,27 @@ fn installed_project_from_row(
|
||||
relative_path: file.relative_path.clone(),
|
||||
project_id: entry.project_id.clone(),
|
||||
version_id: entry.version_id.clone(),
|
||||
source_kind: entry.source_kind,
|
||||
enabled: entry.enabled && file.enabled,
|
||||
})
|
||||
}
|
||||
|
||||
async fn is_shared_instance_member(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<bool> {
|
||||
let Some(metadata) =
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, &state.pool)
|
||||
.await?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
Ok(metadata
|
||||
.shared_instance
|
||||
.is_some_and(|attachment| attachment.role.is_member()))
|
||||
}
|
||||
|
||||
async fn dependency_closure(
|
||||
root_versions: Vec<Version>,
|
||||
content_set: &ContentSet,
|
||||
|
||||
@@ -102,11 +102,28 @@ pub(crate) async fn edit_instance(
|
||||
patch: EditInstance,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Instance> {
|
||||
let modifies_content =
|
||||
patch.link.is_some() || patch.content_set_patch.is_some();
|
||||
let should_mark_shared_instance_stale = patch.link.is_some()
|
||||
|| patch.content_set_patch.as_ref().is_some_and(|patch| {
|
||||
patch.source_kind.is_some()
|
||||
|| patch.game_version.is_some()
|
||||
|| patch.loader.is_some()
|
||||
|| patch.loader_version.is_some()
|
||||
});
|
||||
let mut instance = instance_rows::get_instance_by_id(instance_id, pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
if modifies_content
|
||||
&& instance_rows::is_instance_quarantined(instance_id, pool).await?
|
||||
{
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Content in quarantined instances cannot be changed.".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let now = Utc::now();
|
||||
|
||||
apply_instance_patch(&mut instance, &patch, now);
|
||||
@@ -169,6 +186,10 @@ pub(crate) async fn edit_instance(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if should_mark_shared_instance_stale {
|
||||
super::mark_shared_instance_stale(instance_id, pool).await?;
|
||||
}
|
||||
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::state::instances::{
|
||||
ContentSet, Instance, InstanceLaunchOverrides, InstanceLink,
|
||||
adapters::sqlite::instance_rows,
|
||||
SharedInstanceAttachment, adapters::sqlite::instance_rows,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
@@ -10,6 +10,9 @@ pub struct InstanceMetadata {
|
||||
pub instance: Instance,
|
||||
pub applied_content_set: ContentSet,
|
||||
pub link: InstanceLink,
|
||||
pub shared_instance: Option<SharedInstanceAttachment>,
|
||||
#[serde(default)]
|
||||
pub quarantined: bool,
|
||||
pub groups: Vec<String>,
|
||||
pub launch_overrides: InstanceLaunchOverrides,
|
||||
}
|
||||
@@ -25,44 +28,62 @@ pub(crate) async fn get_instance_metadata(
|
||||
instance_id: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Option<InstanceMetadata>> {
|
||||
Ok(
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, pool)
|
||||
.await?
|
||||
.map(Into::into),
|
||||
)
|
||||
let Some(record) =
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, pool).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let quarantined =
|
||||
instance_rows::is_instance_quarantined(instance_id, pool).await?;
|
||||
|
||||
Ok(Some(instance_metadata(record, quarantined)))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_instances_metadata(
|
||||
instance_ids: &[&str],
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Vec<InstanceMetadata>> {
|
||||
Ok(
|
||||
instance_rows::get_instance_metadata_many(instance_ids, pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
let records =
|
||||
instance_rows::get_instance_metadata_many(instance_ids, pool).await?;
|
||||
let quarantined_ids =
|
||||
instance_rows::get_quarantined_instance_ids(pool).await?;
|
||||
|
||||
Ok(records
|
||||
.into_iter()
|
||||
.map(|record| {
|
||||
let quarantined = quarantined_ids.contains(&record.instance.id);
|
||||
instance_metadata(record, quarantined)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn list_instances(
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Vec<InstanceMetadata>> {
|
||||
Ok(instance_rows::list_instance_metadata(pool)
|
||||
.await?
|
||||
let records = instance_rows::list_instance_metadata(pool).await?;
|
||||
let quarantined_ids =
|
||||
instance_rows::get_quarantined_instance_ids(pool).await?;
|
||||
|
||||
Ok(records
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.map(|record| {
|
||||
let quarantined = quarantined_ids.contains(&record.instance.id);
|
||||
instance_metadata(record, quarantined)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
impl From<instance_rows::InstanceMetadataRecord> for InstanceMetadata {
|
||||
fn from(record: instance_rows::InstanceMetadataRecord) -> Self {
|
||||
Self {
|
||||
instance: record.instance,
|
||||
applied_content_set: record.applied_content_set,
|
||||
link: record.link,
|
||||
groups: record.groups,
|
||||
launch_overrides: record.launch_overrides,
|
||||
}
|
||||
fn instance_metadata(
|
||||
record: instance_rows::InstanceMetadataRecord,
|
||||
quarantined: bool,
|
||||
) -> InstanceMetadata {
|
||||
InstanceMetadata {
|
||||
instance: record.instance,
|
||||
applied_content_set: record.applied_content_set,
|
||||
link: record.link,
|
||||
shared_instance: record.shared_instance,
|
||||
quarantined,
|
||||
groups: record.groups,
|
||||
launch_overrides: record.launch_overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +528,7 @@ pub(crate) async fn dependencies_to_content_items(
|
||||
has_update: false,
|
||||
update_version_id: None,
|
||||
date_added: None,
|
||||
source_kind: None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -738,6 +739,7 @@ async fn content_projects_for_scope(
|
||||
size: file.size,
|
||||
metadata: file_metadata_from_entry_or_cache(entry, metadata),
|
||||
project_type,
|
||||
source_kind: entry.map(|entry| entry.source_kind),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -897,9 +899,13 @@ async fn content_files_to_content_items(
|
||||
date_published: Some(version.date_published.to_rfc3339()),
|
||||
}),
|
||||
owner,
|
||||
has_update: file.update_version_id.is_some(),
|
||||
has_update: file.update_version_id.is_some()
|
||||
&& !file.source_kind.is_some_and(
|
||||
ContentSourceKind::is_shared_instance_managed,
|
||||
),
|
||||
update_version_id: file.update_version_id.clone(),
|
||||
date_added: modification_times[index].clone(),
|
||||
source_kind: file.source_kind,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1089,6 +1095,10 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> {
|
||||
version_id: Some(version_id),
|
||||
..
|
||||
} => Some((project_id.clone(), version_id.clone())),
|
||||
InstanceLink::SharedInstance {
|
||||
modpack_project_id: Some(project_id),
|
||||
modpack_version_id: Some(version_id),
|
||||
} => Some((project_id.clone(), version_id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1103,6 +1113,10 @@ fn linked_modpack_source_kind(
|
||||
InstanceLink::ServerProjectModpack { .. } => {
|
||||
Some(ContentSourceKind::ServerProject)
|
||||
}
|
||||
InstanceLink::SharedInstance {
|
||||
modpack_project_id: Some(_),
|
||||
modpack_version_id: Some(_),
|
||||
} => Some(ContentSourceKind::ModrinthModpack),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1348,12 +1362,7 @@ async fn get_modpack_identifiers(
|
||||
}
|
||||
|
||||
fn project_type_from_api_name(project_type: &str) -> ProjectType {
|
||||
match project_type {
|
||||
"resourcepack" => ProjectType::ResourcePack,
|
||||
"shader" => ProjectType::ShaderPack,
|
||||
"datapack" => ProjectType::DataPack,
|
||||
_ => ProjectType::Mod,
|
||||
}
|
||||
ProjectType::from_name(project_type).unwrap_or(ProjectType::Mod)
|
||||
}
|
||||
|
||||
fn sort_content_items(items: &mut [ContentItem]) {
|
||||
|
||||
@@ -41,3 +41,9 @@ mod check_content_updates;
|
||||
|
||||
mod apply_content_update;
|
||||
pub(crate) use self::apply_content_update::*;
|
||||
|
||||
mod shared_instance;
|
||||
pub(crate) use self::shared_instance::{
|
||||
attach_shared_instance, clear_shared_instance, mark_shared_instance_stale,
|
||||
quarantine_shared_instance, set_shared_instance_sync_status,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
use crate::state::instances::adapters::sqlite::{content_rows, instance_rows};
|
||||
use crate::state::instances::{
|
||||
ContentSetRemoteRef, ContentSetRemoteRefType, ContentSetSyncProvider,
|
||||
ContentSetSyncState, ContentSetSyncStatus, InstanceLink,
|
||||
SharedInstanceAttachment, SharedInstanceAttachmentInput,
|
||||
SharedInstanceRole,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub(crate) async fn attach_shared_instance(
|
||||
instance_id: &str,
|
||||
input: SharedInstanceAttachmentInput,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
let metadata =
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let content_set_id = metadata.applied_content_set.id;
|
||||
let attachment = SharedInstanceAttachment::from(input);
|
||||
let sync_state =
|
||||
shared_sync_state(&content_set_id, &attachment, Some(Utc::now()));
|
||||
|
||||
let mut tx = pool.begin().await?;
|
||||
instance_rows::set_shared_instance_attachment(
|
||||
instance_id,
|
||||
Some(&attachment),
|
||||
&mut tx,
|
||||
)
|
||||
.await?;
|
||||
content_rows::upsert_content_set_remote_ref(
|
||||
&ContentSetRemoteRef {
|
||||
content_set_id: content_set_id.clone(),
|
||||
ref_type: ContentSetRemoteRefType::SharedContentSet,
|
||||
ref_id: attachment.id.clone(),
|
||||
},
|
||||
&mut tx,
|
||||
)
|
||||
.await?;
|
||||
content_rows::upsert_content_set_sync_state(&sync_state, &mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_shared_instance(
|
||||
instance_id: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
detach_shared_instance(instance_id, false, pool).await
|
||||
}
|
||||
|
||||
pub(crate) async fn quarantine_shared_instance(
|
||||
instance_id: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
detach_shared_instance(instance_id, true, pool).await
|
||||
}
|
||||
|
||||
async fn detach_shared_instance(
|
||||
instance_id: &str,
|
||||
quarantine: bool,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
let Some(metadata) =
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, pool).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let content_set_id = metadata.applied_content_set.id;
|
||||
let retained_modpack_link = match metadata.link {
|
||||
InstanceLink::SharedInstance {
|
||||
modpack_project_id: Some(project_id),
|
||||
modpack_version_id: Some(version_id),
|
||||
} => Some(InstanceLink::ModrinthModpack {
|
||||
project_id,
|
||||
version_id,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut tx = pool.begin().await?;
|
||||
instance_rows::set_shared_instance_attachment(instance_id, None, &mut tx)
|
||||
.await?;
|
||||
if quarantine {
|
||||
instance_rows::set_instance_quarantined(instance_id, true, &mut tx)
|
||||
.await?;
|
||||
}
|
||||
if let Some(link) = retained_modpack_link.as_ref() {
|
||||
instance_rows::upsert_instance_link(instance_id, link, &mut tx).await?;
|
||||
}
|
||||
content_rows::delete_content_set_remote_ref(
|
||||
&content_set_id,
|
||||
ContentSetRemoteRefType::SharedContentSet,
|
||||
&mut tx,
|
||||
)
|
||||
.await?;
|
||||
content_rows::delete_content_set_sync_state(&content_set_id, &mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_shared_instance_sync_status(
|
||||
instance_id: &str,
|
||||
status: ContentSetSyncStatus,
|
||||
applied_version: Option<i32>,
|
||||
latest_version: Option<i32>,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
let metadata =
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let Some(mut attachment) = metadata.shared_instance else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
attachment.status = status;
|
||||
attachment.applied_version = applied_version;
|
||||
attachment.latest_version = latest_version;
|
||||
|
||||
let sync_state = shared_sync_state(
|
||||
&metadata.applied_content_set.id,
|
||||
&attachment,
|
||||
Some(Utc::now()),
|
||||
);
|
||||
let mut tx = pool.begin().await?;
|
||||
content_rows::upsert_content_set_sync_state(&sync_state, &mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_shared_instance_stale(
|
||||
instance_id: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
let Some(metadata) =
|
||||
instance_rows::get_instance_metadata_by_id(instance_id, pool).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(attachment) = metadata.shared_instance else {
|
||||
return Ok(());
|
||||
};
|
||||
if attachment.role != SharedInstanceRole::Owner {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
set_shared_instance_sync_status(
|
||||
instance_id,
|
||||
ContentSetSyncStatus::Stale,
|
||||
attachment.applied_version,
|
||||
attachment.latest_version,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn shared_sync_state(
|
||||
content_set_id: &str,
|
||||
attachment: &SharedInstanceAttachment,
|
||||
checked_at: Option<chrono::DateTime<Utc>>,
|
||||
) -> ContentSetSyncState {
|
||||
ContentSetSyncState {
|
||||
content_set_id: content_set_id.to_string(),
|
||||
provider: ContentSetSyncProvider::SharedInstance,
|
||||
applied_update_id: attachment
|
||||
.applied_version
|
||||
.map(|value| value.to_string()),
|
||||
latest_available_update_id: attachment
|
||||
.latest_version
|
||||
.map(|value| value.to_string()),
|
||||
checked_at,
|
||||
status: attachment.status,
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ pub(crate) async fn sync_instance_content_files(
|
||||
let now = Utc::now();
|
||||
let mut files = Vec::new();
|
||||
let mut present_without_hash_ids = Vec::new();
|
||||
let mut restored_without_hash = false;
|
||||
|
||||
for file in scanned {
|
||||
let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
|
||||
@@ -82,6 +83,7 @@ pub(crate) async fn sync_instance_content_files(
|
||||
let Some(hash) = hashes_by_key.get(hash_key) else {
|
||||
if let Some(existing_file) = existing_file {
|
||||
present_without_hash_ids.push(existing_file.id.clone());
|
||||
restored_without_hash |= existing_file.missing;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
@@ -102,6 +104,19 @@ pub(crate) async fn sync_instance_content_files(
|
||||
});
|
||||
}
|
||||
|
||||
let content_changed = !missing_file_ids.is_empty()
|
||||
|| restored_without_hash
|
||||
|| files.iter().any(|file| {
|
||||
existing_files_by_path.get(&file.relative_path).is_none_or(
|
||||
|existing| {
|
||||
existing.missing
|
||||
|| existing.enabled != file.enabled
|
||||
|| existing.sha1 != file.sha1
|
||||
|| existing.size != file.size
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let mut tx = state.pool.begin().await?;
|
||||
for file_id in missing_file_ids {
|
||||
sqlite::content_rows::set_instance_file_missing(
|
||||
@@ -129,6 +144,10 @@ pub(crate) async fn sync_instance_content_files(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if content_changed {
|
||||
super::mark_shared_instance_stale(&instance.id, &state.pool).await?;
|
||||
}
|
||||
|
||||
Ok(stored_files)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::ContentSourceKind;
|
||||
use crate::state::{Project, ProjectType, Version};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -15,6 +16,7 @@ pub struct ContentItem {
|
||||
pub has_update: bool,
|
||||
pub update_version_id: Option<String>,
|
||||
pub date_added: Option<String>,
|
||||
pub source_kind: Option<ContentSourceKind>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -10,6 +10,10 @@ pub use self::commands::{
|
||||
AppliedContentSetPatch, CreateInstance, EditInstance,
|
||||
InstanceLaunchOverridesPatch, InstanceMetadata,
|
||||
};
|
||||
pub(crate) use self::commands::{
|
||||
attach_shared_instance, clear_shared_instance, mark_shared_instance_stale,
|
||||
quarantine_shared_instance, set_shared_instance_sync_status,
|
||||
};
|
||||
pub(crate) use self::commands::{
|
||||
create_instance, edit_instance, get_instance, get_instances_metadata,
|
||||
list_instances, refresh_all_instances, remove_instance,
|
||||
|
||||
@@ -16,6 +16,15 @@ pub enum ContentSourceKind {
|
||||
}
|
||||
|
||||
impl ContentSourceKind {
|
||||
pub fn is_shared_instance_managed(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::SharedInstance
|
||||
| Self::ModrinthModpack
|
||||
| Self::ImportedModpack
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Local => "local",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::ContentSetSyncStatus;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InstanceLink {
|
||||
@@ -32,7 +34,85 @@ pub enum InstanceLink {
|
||||
version_number: Option<String>,
|
||||
filename: Option<String>,
|
||||
},
|
||||
/// Modpack provenance installed by a shared instance. Remote membership,
|
||||
/// manager identity, and synchronization state belong to
|
||||
/// [`SharedInstanceAttachment`].
|
||||
SharedInstance {
|
||||
shared_instance_id: Uuid,
|
||||
modpack_project_id: Option<String>,
|
||||
modpack_version_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SharedInstanceRole {
|
||||
Owner,
|
||||
Member,
|
||||
}
|
||||
|
||||
impl SharedInstanceRole {
|
||||
pub fn is_member(self) -> bool {
|
||||
self == Self::Member
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Owner => "owner",
|
||||
Self::Member => "member",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_stored_str(value: &str) -> crate::Result<Self> {
|
||||
match value {
|
||||
"owner" => Ok(Self::Owner),
|
||||
"member" => Ok(Self::Member),
|
||||
other => Err(super::unknown_value("shared instance role", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
/// The remote shared-instance relationship for a local instance. This is
|
||||
/// independent from the optional modpack provenance stored in [`InstanceLink`].
|
||||
pub struct SharedInstanceAttachment {
|
||||
pub id: String,
|
||||
pub role: SharedInstanceRole,
|
||||
pub manager_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub server_manager_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub server_manager_icon_url: Option<String>,
|
||||
pub linked_user_id: Option<String>,
|
||||
pub status: ContentSetSyncStatus,
|
||||
pub applied_version: Option<i32>,
|
||||
pub latest_version: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SharedInstanceAttachmentInput {
|
||||
pub id: String,
|
||||
pub role: SharedInstanceRole,
|
||||
pub manager_id: Option<String>,
|
||||
pub server_manager_name: Option<String>,
|
||||
pub server_manager_icon_url: Option<String>,
|
||||
pub linked_user_id: Option<String>,
|
||||
pub status: ContentSetSyncStatus,
|
||||
pub applied_version: Option<i32>,
|
||||
pub latest_version: Option<i32>,
|
||||
}
|
||||
|
||||
impl From<SharedInstanceAttachmentInput> for SharedInstanceAttachment {
|
||||
fn from(input: SharedInstanceAttachmentInput) -> Self {
|
||||
Self {
|
||||
id: input.id,
|
||||
role: input.role,
|
||||
manager_id: input.manager_id,
|
||||
server_manager_name: input.server_manager_name,
|
||||
server_manager_icon_url: input.server_manager_icon_url,
|
||||
linked_user_id: input.linked_user_id,
|
||||
status: input.status,
|
||||
applied_version: input.applied_version,
|
||||
latest_version: input.latest_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::State;
|
||||
use crate::api::instance::CONFIG_DIRECTORY;
|
||||
use crate::event::InstancePayloadType;
|
||||
use crate::event::emit::{emit_instance, emit_warning};
|
||||
use crate::state::{
|
||||
@@ -128,17 +129,41 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
Some(InstancePayloadType::WorldUpdated {
|
||||
world,
|
||||
})
|
||||
} else if first_file_name
|
||||
.as_ref()
|
||||
.is_none_or(|x| *x != "saves")
|
||||
{
|
||||
} else if first_file_name.as_ref().is_none_or(
|
||||
|x| *x != "saves" && *x != CONFIG_DIRECTORY,
|
||||
) {
|
||||
Some(InstancePayloadType::Synced)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(event) = event {
|
||||
let emit_instance_id = instance_id.clone();
|
||||
let mark_shared_stale = first_file_name
|
||||
.as_ref()
|
||||
.is_some_and(|name| {
|
||||
ProjectType::iterator().any(
|
||||
|project_type| {
|
||||
*name
|
||||
== project_type
|
||||
.get_folder()
|
||||
},
|
||||
)
|
||||
});
|
||||
tokio::spawn(async move {
|
||||
if mark_shared_stale
|
||||
&& let Ok(state) =
|
||||
State::get().await
|
||||
&& let Err(error) =
|
||||
crate::state::mark_shared_instance_stale(
|
||||
&emit_instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to mark shared instance stale after filesystem sync: {error}"
|
||||
);
|
||||
}
|
||||
let _ = emit_instance(
|
||||
&emit_instance_id,
|
||||
event,
|
||||
@@ -194,10 +219,11 @@ pub(crate) async fn watch_instance_folder(
|
||||
}
|
||||
|
||||
let mut to_watch = Vec::new();
|
||||
for sub_path in ProjectType::iterator()
|
||||
.map(|x| x.get_folder())
|
||||
.chain(["crash-reports", "saves"])
|
||||
{
|
||||
for sub_path in ProjectType::iterator().map(|x| x.get_folder()).chain([
|
||||
"crash-reports",
|
||||
"saves",
|
||||
CONFIG_DIRECTORY,
|
||||
]) {
|
||||
let full_path = full_instance_path.join(sub_path);
|
||||
|
||||
let meta = tokio::fs::symlink_metadata(&full_path).await;
|
||||
|
||||
@@ -195,6 +195,10 @@ pub const fn get_login_url() -> &'static str {
|
||||
concat!(env!("MODRINTH_URL"), "auth/sign-in")
|
||||
}
|
||||
|
||||
pub const fn get_signup_url() -> &'static str {
|
||||
concat!(env!("MODRINTH_URL"), "auth/sign-up")
|
||||
}
|
||||
|
||||
pub async fn finish_login_flow(
|
||||
code: &str,
|
||||
semaphore: &FetchSemaphore,
|
||||
|
||||
Reference in New Issue
Block a user