mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 10:34:53 +00:00
feat: instances v2 (#6431)
* feat: base of instances v2 * feat: use old profiles with compat layer * prototype: instances v2 * fix: install_from using profile * fix: skins migration fix * fix: frontend still using profile path * fix: add update proj multiselect guard * fix: cargo fmt * fix: content missing fields * feat: break up app-lib/api/instance.rs * fix: check_content_updates mismatch * fix: updater modal cleanup w/new structure * feat: better update all handling * fix: remove preview_update_all * fix: feedback on bulk update + lint * fix: rem transitions * fix: change to jsonb * feat: app db backup after update * fix: lint * fix: sqlx prepare + use sqlx macros * fix: lint * fix: bugs * feat: defuck the installing process up * fix: bug of hell * fix: shear * fix: fmt * fix: install progress spacing + change mc/content/overrides to bytes * fix: lint * fix: prepr * fix: navtabs anim not working in app * fix: worlds.vue improvements + browse page fixes * feat: optimise queries + adapter fns * fix: lint * fix: lint * feat: shared modrinth-content-management crate (#6469) * feat: disable warnings setting * feat: add instances shortcuts (#6329) * Add modrinth://launch deep link to start a profile Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck. * Change route to /launch/profile/{id} for future extensibility * fix: ensure profile path is url decoded * fix: URL-decode profile path from deep link * fix: use urlencoding crate for URL decoding * feat: implement app instance shortcuts * feat: change windows shortcut creation to use windows api instead * feat: implement creating a shortcut launching world/server * format * fmt * fix multiline inline tables * pnpm prepr * feat: move create shortcut to last item * refactor: split up shortcuts.rs for individual platforms * refactor: turn profile launch url into url type * use string literal and add safety comment * pt2 * refactor: rename anything that's profile into instance * update mac shortcut --------- Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com> --------- Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
co-authored by
DJCheesusReal
Truman Gao
parent
ef4044534f
commit
734720e11e
@@ -0,0 +1,852 @@
|
||||
use crate::state::instances::{
|
||||
ContentRequirement, ContentSourceKind, Instance, InstanceFile,
|
||||
adapters::sqlite::{content_rows, instance_rows},
|
||||
};
|
||||
use crate::state::{
|
||||
CacheBehaviour, CachedEntry, Dependency, DependencyType, KnownModrinthFile,
|
||||
ModLoader, ProjectType, State, Version, cache_file_hash,
|
||||
};
|
||||
use crate::util::fetch::{self, DownloadMeta, DownloadReason};
|
||||
use crate::util::io;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use modrinth_content_management::{
|
||||
ContentMetadataProvider, ContentType, Error as ResolveError,
|
||||
ResolutionPreferences, ResolveContentPlan, ResolveContentRequest,
|
||||
ResolvedContent,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) struct ContentScope {
|
||||
pub instance: Instance,
|
||||
pub content_set_id: String,
|
||||
}
|
||||
|
||||
pub(crate) struct InstalledContentFile {
|
||||
pub relative_path: String,
|
||||
pub project_id: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct DownloadedProjectVersion {
|
||||
pub file_name: String,
|
||||
pub bytes: Bytes,
|
||||
pub sha1: Option<String>,
|
||||
pub project_type: ProjectType,
|
||||
pub project_id: String,
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceInstallProjectRequest {
|
||||
pub project_id: String,
|
||||
pub version_id: Option<String>,
|
||||
pub content_type: ContentType,
|
||||
pub selected: ResolutionPreferences,
|
||||
}
|
||||
|
||||
struct CachedEntryContentProvider<'a> {
|
||||
state: &'a State,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContentMetadataProvider for CachedEntryContentProvider<'_> {
|
||||
async fn get_version(
|
||||
&mut self,
|
||||
version_id: &str,
|
||||
) -> Result<Option<modrinth_content_management::Version>, ResolveError>
|
||||
{
|
||||
let version = CachedEntry::get_version(
|
||||
version_id,
|
||||
self.cache_behaviour,
|
||||
&self.state.pool,
|
||||
&self.state.api_semaphore,
|
||||
)
|
||||
.await
|
||||
.map_err(resolve_provider_error)?;
|
||||
|
||||
Ok(version.map(version_to_resolver))
|
||||
}
|
||||
|
||||
async fn get_project_versions(
|
||||
&mut self,
|
||||
project_id: &str,
|
||||
) -> Result<Vec<modrinth_content_management::Version>, ResolveError> {
|
||||
let versions = CachedEntry::get_project_versions(
|
||||
project_id,
|
||||
self.cache_behaviour,
|
||||
&self.state.pool,
|
||||
&self.state.api_semaphore,
|
||||
)
|
||||
.await
|
||||
.map_err(resolve_provider_error)?;
|
||||
|
||||
Ok(versions
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(version_to_resolver)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_provider_error(error: crate::Error) -> ResolveError {
|
||||
ResolveError::Provider(error.to_string())
|
||||
}
|
||||
|
||||
fn resolver_error(error: ResolveError) -> crate::Error {
|
||||
crate::ErrorKind::InputError(error.to_string()).into()
|
||||
}
|
||||
|
||||
fn version_to_resolver(
|
||||
version: Version,
|
||||
) -> modrinth_content_management::Version {
|
||||
modrinth_content_management::Version {
|
||||
id: version.id,
|
||||
project_id: version.project_id,
|
||||
date_published: version.date_published,
|
||||
dependencies: version
|
||||
.dependencies
|
||||
.into_iter()
|
||||
.map(dependency_to_resolver)
|
||||
.collect(),
|
||||
game_versions: version.game_versions,
|
||||
loaders: version.loaders,
|
||||
}
|
||||
}
|
||||
|
||||
fn dependency_to_resolver(
|
||||
dependency: Dependency,
|
||||
) -> modrinth_content_management::Dependency {
|
||||
modrinth_content_management::Dependency {
|
||||
version_id: dependency.version_id,
|
||||
project_id: dependency.project_id,
|
||||
file_name: dependency.file_name,
|
||||
dependency_type: match dependency.dependency_type {
|
||||
DependencyType::Required => {
|
||||
modrinth_content_management::DependencyType::Required
|
||||
}
|
||||
DependencyType::Optional => {
|
||||
modrinth_content_management::DependencyType::Optional
|
||||
}
|
||||
DependencyType::Incompatible => {
|
||||
modrinth_content_management::DependencyType::Incompatible
|
||||
}
|
||||
DependencyType::Embedded => {
|
||||
modrinth_content_management::DependencyType::Embedded
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn target_preferences(
|
||||
game_version: String,
|
||||
loader: ModLoader,
|
||||
content_type: ContentType,
|
||||
) -> ResolutionPreferences {
|
||||
let loader = match content_type {
|
||||
ContentType::DataPack => "datapack".to_string(),
|
||||
ContentType::ResourcePack => "minecraft".to_string(),
|
||||
ContentType::Shader => "iris".to_string(),
|
||||
_ => loader.as_str().to_string(),
|
||||
};
|
||||
|
||||
ResolutionPreferences {
|
||||
game_versions: vec![game_version],
|
||||
loaders: vec![loader],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_install_plan(
|
||||
instance_id: &str,
|
||||
request: InstanceInstallProjectRequest,
|
||||
state: &State,
|
||||
) -> crate::Result<ResolveContentPlan> {
|
||||
let content_set =
|
||||
content_rows::get_applied_content_set(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Instance {instance_id} has no applied content set"
|
||||
))
|
||||
})?;
|
||||
let existing_project_ids =
|
||||
crate::state::get_installed_project_ids_for_instance(
|
||||
instance_id,
|
||||
None,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
let provider = CachedEntryContentProvider {
|
||||
state,
|
||||
cache_behaviour: Some(CacheBehaviour::MustRevalidate),
|
||||
};
|
||||
let content_type = request.content_type;
|
||||
let request = ResolveContentRequest {
|
||||
project_id: request.project_id,
|
||||
version_id: request.version_id,
|
||||
content_type,
|
||||
selected: request.selected,
|
||||
target: target_preferences(
|
||||
content_set.game_version,
|
||||
content_set.loader,
|
||||
content_type,
|
||||
),
|
||||
existing_project_ids,
|
||||
};
|
||||
|
||||
modrinth_content_management::resolve_content(provider, request)
|
||||
.await
|
||||
.map_err(resolver_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn install_resolved_content_plan(
|
||||
instance_id: &str,
|
||||
plan: &ResolveContentPlan,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
add_resolved_content(
|
||||
instance_id,
|
||||
&plan.primary,
|
||||
DownloadReason::Standalone,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
for dependency in &plan.dependencies {
|
||||
add_resolved_content(
|
||||
instance_id,
|
||||
dependency,
|
||||
DownloadReason::Dependency,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn switch_project_version_with_dependencies(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
version_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
let version = CachedEntry::get_version(
|
||||
version_id,
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unable to install version id {version_id}. Not found."
|
||||
))
|
||||
})?;
|
||||
let content_type = ProjectType::get_from_loaders(version.loaders.clone())
|
||||
.map(ContentType::from)
|
||||
.unwrap_or(ContentType::Mod);
|
||||
let plan = resolve_install_plan(
|
||||
instance_id,
|
||||
InstanceInstallProjectRequest {
|
||||
project_id: version.project_id,
|
||||
version_id: Some(version_id.to_string()),
|
||||
content_type,
|
||||
selected: ResolutionPreferences::default(),
|
||||
},
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let was_disabled = project_path.ends_with(".disabled");
|
||||
let mut new_path = add_project_from_version(
|
||||
instance_id,
|
||||
&plan.primary.version_id,
|
||||
DownloadReason::Update,
|
||||
None,
|
||||
ContentSourceKind::Local,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if was_disabled {
|
||||
new_path =
|
||||
toggle_disable_project(instance_id, &new_path, Some(false), state)
|
||||
.await?;
|
||||
}
|
||||
|
||||
for dependency in &plan.dependencies {
|
||||
add_resolved_content(
|
||||
instance_id,
|
||||
dependency,
|
||||
DownloadReason::Dependency,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if new_path != project_path {
|
||||
remove_project(instance_id, project_path, state).await?;
|
||||
}
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
async fn add_resolved_content(
|
||||
instance_id: &str,
|
||||
content: &ResolvedContent,
|
||||
reason: DownloadReason,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
add_project_from_version(
|
||||
instance_id,
|
||||
&content.version_id,
|
||||
reason,
|
||||
content.dependent_on_version_id.clone(),
|
||||
ContentSourceKind::Local,
|
||||
state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_content_scope(
|
||||
instance_id: &str,
|
||||
content_set_id: Option<&str>,
|
||||
state: &State,
|
||||
) -> crate::Result<ContentScope> {
|
||||
let instance = instance_rows::get_instance_by_id(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let content_set_id = match content_set_id {
|
||||
Some(id) => id.to_string(),
|
||||
None => instance.applied_content_set_id.clone().ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Instance {} has no applied content set",
|
||||
instance.id
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
Ok(ContentScope {
|
||||
instance,
|
||||
content_set_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn add_project_from_version(
|
||||
instance_id: &str,
|
||||
version_id: &str,
|
||||
reason: DownloadReason,
|
||||
dependent_on_version_id: Option<String>,
|
||||
source_kind: ContentSourceKind,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
let downloaded = download_project_version(
|
||||
instance_id,
|
||||
version_id,
|
||||
reason,
|
||||
dependent_on_version_id,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
add_downloaded_project_version(instance_id, downloaded, source_kind, state)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn download_project_version(
|
||||
instance_id: &str,
|
||||
version_id: &str,
|
||||
reason: DownloadReason,
|
||||
dependent_on_version_id: Option<String>,
|
||||
state: &State,
|
||||
) -> crate::Result<DownloadedProjectVersion> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let content_set =
|
||||
content_rows::get_content_set(&scope.content_set_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown content set {}",
|
||||
scope.content_set_id
|
||||
))
|
||||
})?;
|
||||
let version = CachedEntry::get_version(
|
||||
version_id,
|
||||
None,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unable to install version id {version_id}. Not found."
|
||||
))
|
||||
})?;
|
||||
let file = version
|
||||
.files
|
||||
.iter()
|
||||
.find(|file| file.primary)
|
||||
.or_else(|| version.files.first())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(
|
||||
"No files for input version present!".to_string(),
|
||||
)
|
||||
})?;
|
||||
let download_meta = DownloadMeta {
|
||||
reason,
|
||||
game_version: content_set.game_version,
|
||||
loader: content_set.loader.as_str().to_string(),
|
||||
dependent_on: dependent_on_version_id,
|
||||
};
|
||||
let bytes = fetch::fetch(
|
||||
&file.url,
|
||||
file.hashes.get("sha1").map(|hash| hash.as_str()),
|
||||
Some(&download_meta),
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
let project_type = ProjectType::get_from_loaders(version.loaders.clone())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unable to infer project type for version {version_id}"
|
||||
))
|
||||
})?;
|
||||
let project_id = version.project_id.clone();
|
||||
let version_id = version.id.clone();
|
||||
|
||||
Ok(DownloadedProjectVersion {
|
||||
file_name: file.filename.clone(),
|
||||
bytes,
|
||||
sha1: file.hashes.get("sha1").cloned(),
|
||||
project_type,
|
||||
project_id,
|
||||
version_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn add_downloaded_project_version(
|
||||
instance_id: &str,
|
||||
downloaded: DownloadedProjectVersion,
|
||||
source_kind: ContentSourceKind,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
let DownloadedProjectVersion {
|
||||
file_name,
|
||||
bytes,
|
||||
sha1,
|
||||
project_type,
|
||||
project_id,
|
||||
version_id,
|
||||
} = downloaded;
|
||||
|
||||
add_project_bytes(
|
||||
instance_id,
|
||||
&file_name,
|
||||
bytes,
|
||||
sha1.as_deref(),
|
||||
Some(project_type),
|
||||
source_kind,
|
||||
Some(project_id.as_str()),
|
||||
Some(version_id.as_str()),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn add_project_from_path(
|
||||
instance_id: &str,
|
||||
path: &Path,
|
||||
project_type: Option<ProjectType>,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
let file = io::read(path).await?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
add_project_bytes(
|
||||
instance_id,
|
||||
&file_name,
|
||||
Bytes::from(file),
|
||||
None,
|
||||
project_type,
|
||||
ContentSourceKind::Local,
|
||||
None,
|
||||
None,
|
||||
state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn add_project_bytes(
|
||||
instance_id: &str,
|
||||
file_name: &str,
|
||||
bytes: Bytes,
|
||||
hash: Option<&str>,
|
||||
project_type: Option<ProjectType>,
|
||||
source_kind: ContentSourceKind,
|
||||
project_id: Option<&str>,
|
||||
version_id: Option<&str>,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let project_type = match project_type {
|
||||
Some(project_type) => project_type,
|
||||
None => infer_project_type(&bytes)?,
|
||||
};
|
||||
let relative_path = format!("{}/{}", project_type.get_folder(), file_name);
|
||||
let full_path =
|
||||
instance_full_path(state, &scope.instance).join(&relative_path);
|
||||
let sha1 = match hash {
|
||||
Some(hash) => hash.to_string(),
|
||||
None => fetch::sha1_async(bytes.clone()).await?,
|
||||
};
|
||||
|
||||
cache_file_hash(
|
||||
bytes.clone(),
|
||||
&scope.instance.id,
|
||||
&relative_path,
|
||||
Some(&sha1),
|
||||
Some(project_type),
|
||||
project_id.zip(version_id).map(|(project_id, version_id)| {
|
||||
KnownModrinthFile {
|
||||
project_id,
|
||||
version_id,
|
||||
}
|
||||
}),
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
fetch::write(&full_path, &bytes, &state.io_semaphore).await?;
|
||||
|
||||
let file = content_rows::upsert_instance_file_from_parts(
|
||||
content_rows::UpsertInstanceFile {
|
||||
instance_id: &scope.instance.id,
|
||||
relative_path: &relative_path,
|
||||
file_name,
|
||||
enabled: !relative_path.ends_with(".disabled"),
|
||||
sha1: &sha1,
|
||||
size: bytes.len() as u64,
|
||||
missing: false,
|
||||
},
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
upsert_entry_for_file(
|
||||
&scope,
|
||||
&file,
|
||||
project_type,
|
||||
project_id,
|
||||
version_id,
|
||||
source_kind,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(relative_path)
|
||||
}
|
||||
|
||||
pub(crate) async fn record_project_file(
|
||||
instance_id: &str,
|
||||
relative_path: &str,
|
||||
sha1: &str,
|
||||
size: u64,
|
||||
project_type: ProjectType,
|
||||
source_kind: ContentSourceKind,
|
||||
project_id: Option<&str>,
|
||||
version_id: Option<&str>,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let file_name = Path::new(relative_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let file = content_rows::upsert_instance_file_from_parts(
|
||||
content_rows::UpsertInstanceFile {
|
||||
instance_id: &scope.instance.id,
|
||||
relative_path,
|
||||
file_name: &file_name,
|
||||
enabled: !relative_path.ends_with(".disabled"),
|
||||
sha1,
|
||||
size,
|
||||
missing: false,
|
||||
},
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
upsert_entry_for_file(
|
||||
&scope,
|
||||
&file,
|
||||
project_type,
|
||||
project_id,
|
||||
version_id,
|
||||
source_kind,
|
||||
&state.pool,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn toggle_disable_project(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
desired_enabled: Option<bool>,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let base = instance_full_path(state, &scope.instance);
|
||||
let trimmed = project_path.trim_end_matches(".disabled");
|
||||
let current_path = if base.join(project_path).exists() {
|
||||
project_path.to_string()
|
||||
} else if base.join(format!("{trimmed}.disabled")).exists() {
|
||||
format!("{trimmed}.disabled")
|
||||
} else if base.join(trimmed).exists() {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
return Err(crate::ErrorKind::FSError(format!(
|
||||
"Could not find project file for '{project_path}' in instance"
|
||||
))
|
||||
.into());
|
||||
};
|
||||
let current_enabled = !current_path.ends_with(".disabled");
|
||||
let enabled = desired_enabled.unwrap_or(!current_enabled);
|
||||
let new_path = if enabled {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
format!("{trimmed}.disabled")
|
||||
};
|
||||
|
||||
if current_path != new_path {
|
||||
io::rename_or_move(&base.join(¤t_path), &base.join(&new_path))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let file_name = Path::new(&new_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let file = match content_rows::rename_instance_file(
|
||||
&scope.instance.id,
|
||||
¤t_path,
|
||||
&new_path,
|
||||
&file_name,
|
||||
enabled,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(file) => file,
|
||||
None if current_path != project_path => {
|
||||
match content_rows::rename_instance_file(
|
||||
&scope.instance.id,
|
||||
project_path,
|
||||
&new_path,
|
||||
&file_name,
|
||||
enabled,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(file) => file,
|
||||
None => index_existing_file(&scope, &new_path, state).await?,
|
||||
}
|
||||
}
|
||||
None => index_existing_file(&scope, &new_path, state).await?,
|
||||
};
|
||||
content_rows::set_content_entry_enabled_for_file(
|
||||
&scope.content_set_id,
|
||||
&file.id,
|
||||
enabled,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_project(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let base = instance_full_path(state, &scope.instance);
|
||||
let file = content_rows::get_instance_file_by_relative_path(
|
||||
&scope.instance.id,
|
||||
project_path,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
io::remove_file(base.join(project_path)).await?;
|
||||
|
||||
if let Some(file) = file {
|
||||
content_rows::remove_content_entries_for_file(
|
||||
&scope.content_set_id,
|
||||
&file.id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
content_rows::remove_instance_file_by_relative_path(
|
||||
&scope.instance.id,
|
||||
project_path,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn list_project_files(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<Vec<InstalledContentFile>> {
|
||||
let scope = resolve_content_scope(instance_id, None, state).await?;
|
||||
let entries =
|
||||
content_rows::get_content_entries(&scope.content_set_id, &state.pool)
|
||||
.await?;
|
||||
let files =
|
||||
content_rows::get_instance_files(&scope.instance.id, &state.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|file| (file.id.clone(), file))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
|
||||
Ok(entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let file = files.get(entry.file_id.as_ref()?)?;
|
||||
Some(InstalledContentFile {
|
||||
relative_path: file.relative_path.clone(),
|
||||
project_id: entry.project_id,
|
||||
enabled: entry.enabled && file.enabled,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn instance_full_path(
|
||||
state: &State,
|
||||
instance: &Instance,
|
||||
) -> PathBuf {
|
||||
state.directories.instances_dir().join(&instance.path)
|
||||
}
|
||||
|
||||
async fn index_existing_file(
|
||||
scope: &ContentScope,
|
||||
relative_path: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<InstanceFile> {
|
||||
let full_path =
|
||||
instance_full_path(state, &scope.instance).join(relative_path);
|
||||
let (size, sha1) = fetch::sha1_file_async(&full_path).await?;
|
||||
let file_name = Path::new(relative_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let project_type = ProjectType::get_from_parent_folder(relative_path)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unable to infer project type from {relative_path}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let file = content_rows::upsert_instance_file_from_parts(
|
||||
content_rows::UpsertInstanceFile {
|
||||
instance_id: &scope.instance.id,
|
||||
relative_path,
|
||||
file_name: &file_name,
|
||||
enabled: !relative_path.ends_with(".disabled"),
|
||||
sha1: &sha1,
|
||||
size,
|
||||
missing: false,
|
||||
},
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
upsert_entry_for_file(
|
||||
scope,
|
||||
&file,
|
||||
project_type,
|
||||
None,
|
||||
None,
|
||||
ContentSourceKind::Local,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
async fn upsert_entry_for_file(
|
||||
scope: &ContentScope,
|
||||
file: &InstanceFile,
|
||||
project_type: ProjectType,
|
||||
project_id: Option<&str>,
|
||||
version_id: Option<&str>,
|
||||
source_kind: ContentSourceKind,
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
content_rows::upsert_content_entry_from_parts(
|
||||
content_rows::UpsertContentEntry {
|
||||
instance_id: &scope.instance.id,
|
||||
content_set_id: &scope.content_set_id,
|
||||
file_id: Some(&file.id),
|
||||
project_type,
|
||||
project_id,
|
||||
version_id,
|
||||
source_kind,
|
||||
server_requirement: ContentRequirement::Required,
|
||||
client_requirement: ContentRequirement::Required,
|
||||
enabled: file.enabled,
|
||||
},
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn infer_project_type(bytes: &Bytes) -> crate::Result<ProjectType> {
|
||||
let cursor = std::io::Cursor::new(&**bytes);
|
||||
let mut archive = zip::ZipArchive::new(cursor).map_err(|_| {
|
||||
crate::ErrorKind::InputError(
|
||||
"Unable to infer project type for input file".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if archive.by_name("fabric.mod.json").is_ok()
|
||||
|| archive.by_name("quilt.mod.json").is_ok()
|
||||
|| archive.by_name("META-INF/neoforge.mods.toml").is_ok()
|
||||
|| archive.by_name("META-INF/mods.toml").is_ok()
|
||||
|| archive.by_name("mcmod.info").is_ok()
|
||||
{
|
||||
Ok(ProjectType::Mod)
|
||||
} else if archive.by_name("pack.mcmeta").is_ok() {
|
||||
if archive.file_names().any(|name| name.starts_with("data/")) {
|
||||
Ok(ProjectType::DataPack)
|
||||
} else {
|
||||
Ok(ProjectType::ResourcePack)
|
||||
}
|
||||
} else if archive
|
||||
.file_names()
|
||||
.any(|name| name.starts_with("shaders/"))
|
||||
{
|
||||
Ok(ProjectType::ShaderPack)
|
||||
} else {
|
||||
Err(crate::ErrorKind::InputError(
|
||||
"Unable to infer project type for input file".to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user