mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
qa: shared instances post release (#6893)
* fix: 20 user cap * qa: invite modal dropdown + other + clamp limits * fix: hide filters if just one type * feat: update req banner * feat: invite management frontend * fix: moderation issues with shared instances * feat: instance icon improvements * fix: better offline handling * fix: lint * feat: show avatar in install to play modal * feat: smaller qa points * fix: fmt * fix: fmt * fix: yeet svg * fix: 50 user limit
This commit is contained in:
@@ -4,6 +4,7 @@ mod content;
|
||||
mod content_set_diff;
|
||||
mod export_mrpack;
|
||||
mod get;
|
||||
mod icon;
|
||||
mod install;
|
||||
mod lifecycle;
|
||||
mod paths;
|
||||
@@ -21,9 +22,13 @@ pub use self::export_mrpack::{
|
||||
create_mrpack_json, export_mrpack, get_pack_export_candidates,
|
||||
};
|
||||
pub use self::get::{get, get_many, list};
|
||||
pub use self::icon::edit_icon;
|
||||
pub(crate) use self::icon::{
|
||||
cache_icon, cache_icon_from_path, migrate_legacy_icons,
|
||||
};
|
||||
pub use self::install::get_optimal_jre_key;
|
||||
pub(crate) use self::lifecycle::create;
|
||||
pub use self::lifecycle::{edit, edit_icon, remove};
|
||||
pub use self::lifecycle::{edit, remove};
|
||||
pub use self::paths::{get_full_path, get_mod_full_path};
|
||||
pub use self::projects::{
|
||||
InstallProjectWithDependenciesRequest, add_project_from_path,
|
||||
@@ -42,17 +47,19 @@ pub(crate) use self::shared::{
|
||||
};
|
||||
pub use self::shared::{
|
||||
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
||||
SharedInstanceInviteInstallPreview, SharedInstanceInviteLink,
|
||||
SharedInstanceJoinType, SharedInstancePublishPreview,
|
||||
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
||||
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
||||
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
||||
SharedInstanceInviteLink, SharedInstanceJoinType,
|
||||
SharedInstancePublishPreview, SharedInstanceUpdateDiff,
|
||||
SharedInstanceUpdateDiffType, SharedInstanceUpdatePreview,
|
||||
SharedInstanceUser, SharedInstanceUsers,
|
||||
accept_pending_shared_instance_invite,
|
||||
accept_shared_instance_invite_for_install,
|
||||
can_active_user_use_shared_instances, create_shared_instance_invite_link,
|
||||
decline_pending_shared_instance_invite,
|
||||
get_shared_instance_install_preview, get_shared_instance_publish_preview,
|
||||
get_shared_instance_update_preview, get_shared_instance_users,
|
||||
install_shared_instance, invite_shared_instance_users,
|
||||
publish_shared_instance, remove_shared_instance_users,
|
||||
get_shared_instance_install_preview, get_shared_instance_invites,
|
||||
get_shared_instance_publish_preview, get_shared_instance_update_preview,
|
||||
get_shared_instance_users, install_shared_instance,
|
||||
invite_shared_instance_users, publish_shared_instance,
|
||||
remove_shared_instance_users, revoke_shared_instance_invite,
|
||||
unlink_shared_instance, unpublish_shared_instance, update_shared_instance,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
use crate::event::InstancePayloadType;
|
||||
use crate::event::emit::emit_instance;
|
||||
use crate::state::instances::adapters::sqlite::instance_rows;
|
||||
use crate::state::{EditInstance, State};
|
||||
use crate::util::fetch::{sha1_async, write};
|
||||
use crate::util::io;
|
||||
use bytes::Bytes;
|
||||
use std::fs::File as StdFile;
|
||||
use std::io::{BufRead, BufReader, Cursor, Seek};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const INSTANCE_ICON_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
const INSTANCE_ICON_MAX_DIMENSION: u32 = 512;
|
||||
const INSTANCE_ICON_MAX_SOURCE_DIMENSION: u32 = 8_192;
|
||||
const INSTANCE_ICON_MAX_DECODE_BYTES: u64 = 64 * 1024 * 1024;
|
||||
|
||||
enum LegacyIconAction {
|
||||
Keep,
|
||||
Normalize,
|
||||
Remove,
|
||||
}
|
||||
|
||||
pub async fn edit_icon(
|
||||
instance_id: &str,
|
||||
icon_path: Option<&Path>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let icon_path = if let Some(icon_path) = icon_path {
|
||||
Some(
|
||||
cache_icon_from_path(icon_path, &state)
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
apply_instance_icon(instance_id, icon_path, &state).await
|
||||
}
|
||||
|
||||
pub(crate) async fn cache_icon(
|
||||
bytes: Bytes,
|
||||
state: &State,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let bytes = tokio::task::spawn_blocking(move || {
|
||||
if looks_like_svg(&bytes) {
|
||||
return Err(svg_not_supported_error());
|
||||
}
|
||||
|
||||
normalize_raster(Cursor::new(bytes))
|
||||
})
|
||||
.await??;
|
||||
|
||||
write_cached_icon(bytes, state).await
|
||||
}
|
||||
|
||||
pub(crate) async fn cache_icon_from_path(
|
||||
icon_path: &Path,
|
||||
state: &State,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let icon_path = icon_path.to_path_buf();
|
||||
let bytes = tokio::task::spawn_blocking(move || {
|
||||
let file = StdFile::open(&icon_path).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not open instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let looks_like_svg = {
|
||||
let bytes = reader.fill_buf().map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not inspect instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
looks_like_svg(bytes)
|
||||
};
|
||||
if has_svg_extension(&icon_path) || looks_like_svg {
|
||||
return Err(svg_not_supported_error());
|
||||
}
|
||||
|
||||
normalize_raster(reader)
|
||||
})
|
||||
.await??;
|
||||
|
||||
write_cached_icon(bytes, state).await
|
||||
}
|
||||
|
||||
pub(crate) async fn migrate_legacy_icons() -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instances = instance_rows::list_instances(&state.pool).await?;
|
||||
|
||||
for instance in instances {
|
||||
let Some(icon_path) = instance.icon_path.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let action = match inspect_legacy_icon(Path::new(icon_path)) {
|
||||
Ok(action) => action,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
instance_id = instance.id,
|
||||
icon_path,
|
||||
error = %error,
|
||||
"Failed to inspect legacy instance icon"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match action {
|
||||
LegacyIconAction::Keep => {}
|
||||
LegacyIconAction::Normalize => {
|
||||
if let Err(error) =
|
||||
edit_icon(&instance.id, Some(Path::new(icon_path))).await
|
||||
{
|
||||
tracing::warn!(
|
||||
instance_id = instance.id,
|
||||
icon_path,
|
||||
error = %error,
|
||||
"Failed to normalize legacy instance icon"
|
||||
);
|
||||
}
|
||||
}
|
||||
LegacyIconAction::Remove => {
|
||||
if let Err(error) =
|
||||
apply_instance_icon(&instance.id, None, &state).await
|
||||
{
|
||||
tracing::warn!(
|
||||
instance_id = instance.id,
|
||||
icon_path,
|
||||
error = %error,
|
||||
"Failed to remove legacy SVG instance icon"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_instance_icon(
|
||||
instance_id: &str,
|
||||
icon_path: Option<String>,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
let instance =
|
||||
instance_rows::get_instance_display_info(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
crate::state::edit_instance(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
icon_path: Some(icon_path.clone()),
|
||||
..EditInstance::default()
|
||||
},
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Err(error) = super::shared::sync_shared_instance_icon(
|
||||
instance_id,
|
||||
icon_path.as_deref(),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
instance_id,
|
||||
error = %error,
|
||||
"Failed to sync shared instance icon"
|
||||
);
|
||||
}
|
||||
|
||||
emit_instance(&instance.id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_cached_icon(
|
||||
bytes: Bytes,
|
||||
state: &State,
|
||||
) -> crate::Result<PathBuf> {
|
||||
if bytes.len() >= INSTANCE_ICON_MAX_BYTES {
|
||||
return Err(icon_too_large_error());
|
||||
}
|
||||
|
||||
let hash = sha1_async(bytes.clone()).await?;
|
||||
let path = state
|
||||
.directories
|
||||
.caches_dir()
|
||||
.join("icons")
|
||||
.join(format!("{hash}.png"));
|
||||
write(&path, &bytes, &state.io_semaphore).await?;
|
||||
|
||||
Ok(io::canonicalize(path)?)
|
||||
}
|
||||
|
||||
fn normalize_raster<R>(reader: R) -> crate::Result<Bytes>
|
||||
where
|
||||
R: BufRead + Seek,
|
||||
{
|
||||
let mut reader = image::ImageReader::new(reader)
|
||||
.with_guessed_format()
|
||||
.map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not identify instance icon format: {error}"
|
||||
))
|
||||
})?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_image_width = Some(INSTANCE_ICON_MAX_SOURCE_DIMENSION);
|
||||
limits.max_image_height = Some(INSTANCE_ICON_MAX_SOURCE_DIMENSION);
|
||||
limits.max_alloc = Some(INSTANCE_ICON_MAX_DECODE_BYTES);
|
||||
reader.limits(limits);
|
||||
|
||||
let image = reader.decode().map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not decode instance icon: {error}"
|
||||
))
|
||||
})?;
|
||||
let image = if image.width() > INSTANCE_ICON_MAX_DIMENSION
|
||||
|| image.height() > INSTANCE_ICON_MAX_DIMENSION
|
||||
{
|
||||
image.resize(
|
||||
INSTANCE_ICON_MAX_DIMENSION,
|
||||
INSTANCE_ICON_MAX_DIMENSION,
|
||||
image::imageops::FilterType::Lanczos3,
|
||||
)
|
||||
} else {
|
||||
image
|
||||
};
|
||||
let mut normalized = Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(image.to_rgba8())
|
||||
.write_to(&mut normalized, image::ImageFormat::Png)
|
||||
.map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not encode instance icon as PNG: {error}"
|
||||
))
|
||||
})?;
|
||||
|
||||
validate_normalized_icon(normalized.into_inner())
|
||||
}
|
||||
|
||||
fn inspect_legacy_icon(icon_path: &Path) -> crate::Result<LegacyIconAction> {
|
||||
let metadata = std::fs::metadata(icon_path).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not inspect instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
let file = StdFile::open(icon_path).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not open instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let bytes = reader.fill_buf().map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not inspect instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
if has_svg_extension(icon_path) || looks_like_svg(bytes) {
|
||||
return Ok(LegacyIconAction::Remove);
|
||||
}
|
||||
|
||||
if metadata.len() < INSTANCE_ICON_MAX_BYTES as u64
|
||||
&& image::guess_format(bytes).ok() == Some(image::ImageFormat::Png)
|
||||
{
|
||||
return Ok(LegacyIconAction::Keep);
|
||||
}
|
||||
|
||||
Ok(LegacyIconAction::Normalize)
|
||||
}
|
||||
|
||||
fn validate_normalized_icon(normalized: Vec<u8>) -> crate::Result<Bytes> {
|
||||
if normalized.len() >= INSTANCE_ICON_MAX_BYTES {
|
||||
return Err(icon_too_large_error());
|
||||
}
|
||||
|
||||
Ok(Bytes::from(normalized))
|
||||
}
|
||||
|
||||
fn looks_like_svg(bytes: &[u8]) -> bool {
|
||||
if image::guess_format(bytes).is_ok() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes[..bytes.len().min(1_024)]
|
||||
.windows(4)
|
||||
.any(|window| window.eq_ignore_ascii_case(b"<svg"))
|
||||
}
|
||||
|
||||
fn has_svg_extension(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("svg"))
|
||||
}
|
||||
|
||||
fn icon_too_large_error() -> crate::Error {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Instance icons must be smaller than {INSTANCE_ICON_MAX_BYTES} bytes"
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn svg_not_supported_error() -> crate::Error {
|
||||
crate::ErrorKind::InputError(
|
||||
"SVG instance icons are not supported".to_string(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
@@ -5,8 +5,6 @@ use crate::state::{
|
||||
CreateInstance, EditInstance, InstanceLink, InstanceMetadata, ModLoader,
|
||||
State,
|
||||
};
|
||||
use crate::util::io;
|
||||
use std::path::Path;
|
||||
|
||||
#[tracing::instrument]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -73,60 +71,6 @@ pub async fn edit(
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
pub async fn edit_icon(
|
||||
instance_id: &str,
|
||||
icon_path: Option<&Path>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance =
|
||||
instance_rows::get_instance_display_info(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let icon_path = if let Some(icon) = icon_path {
|
||||
let bytes = io::read(icon).await?;
|
||||
let file = crate::util::fetch::write_cached_icon(
|
||||
&icon.to_string_lossy(),
|
||||
&state.directories.caches_dir(),
|
||||
bytes::Bytes::from(bytes),
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
Some(file.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
crate::state::edit_instance(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
icon_path: Some(icon_path.clone()),
|
||||
..EditInstance::default()
|
||||
},
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Err(error) = super::shared::sync_shared_instance_icon(
|
||||
instance_id,
|
||||
icon_path.as_deref(),
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
instance_id,
|
||||
error = %error,
|
||||
"Failed to sync shared instance icon"
|
||||
);
|
||||
}
|
||||
|
||||
emit_instance(&instance.id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn remove(instance_id: &str) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
@@ -73,6 +73,14 @@ pub(super) struct CreateInstanceInviteResponse {
|
||||
pub(super) id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(super) struct InstanceInviteResponse {
|
||||
pub(super) id: String,
|
||||
pub(super) expiration: DateTime<Utc>,
|
||||
pub(super) max_uses: i32,
|
||||
pub(super) uses: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(super) struct BlacklistStatusResponse {
|
||||
pub(super) blacklisted: bool,
|
||||
@@ -502,6 +510,20 @@ pub(super) async fn delete_remote_invite(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn get_remote_invites(
|
||||
shared_instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<Vec<InstanceInviteResponse>> {
|
||||
request_json(
|
||||
"get_instance_invites",
|
||||
Method::GET,
|
||||
&format!("/instances/{shared_instance_id}/invites"),
|
||||
None,
|
||||
state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn get_shared_instance_invite_info(
|
||||
invite_id: &str,
|
||||
state: &State,
|
||||
@@ -691,9 +713,17 @@ where
|
||||
let body = match response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(error) if strip_response_url => {
|
||||
return Err(error.without_url().into());
|
||||
return Err(crate::ErrorKind::SharedInstancesApiError(
|
||||
error.without_url().to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(crate::ErrorKind::SharedInstancesApiError(
|
||||
error.to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
serde_json::from_str::<T>(&body).map_err(|error| {
|
||||
tracing::warn!(
|
||||
@@ -707,7 +737,7 @@ where
|
||||
error_column = error.column(),
|
||||
"Shared instances API returned an invalid JSON response"
|
||||
);
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
crate::ErrorKind::SharedInstancesApiError(format!(
|
||||
"Shared instances API request {operation} {method} {log_path} returned invalid JSON with status {status}"
|
||||
))
|
||||
.into()
|
||||
@@ -848,7 +878,12 @@ pub(super) async fn send_bytes_request_to_url(
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(body)
|
||||
.send()
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|error| {
|
||||
crate::ErrorKind::SharedInstancesApiError(
|
||||
error.without_url().to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let request_id = response_request_id(&response);
|
||||
@@ -938,9 +973,17 @@ async fn send_request_with_auth_and_log_path(
|
||||
let response = match request.send().await {
|
||||
Ok(response) => response,
|
||||
Err(error) if path != log_path => {
|
||||
return Err(error.without_url().into());
|
||||
return Err(crate::ErrorKind::SharedInstancesApiError(
|
||||
error.without_url().to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(crate::ErrorKind::SharedInstancesApiError(
|
||||
error.to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
if response.status().is_success() {
|
||||
let request_id = response_request_id(&response);
|
||||
@@ -973,10 +1016,14 @@ pub(super) async fn shared_instances_request_error<T>(
|
||||
request_id = request_id.as_deref().unwrap_or("none"),
|
||||
"Shared instances API request failed"
|
||||
);
|
||||
Err(crate::ErrorKind::OtherError(format!(
|
||||
let message = format!(
|
||||
"Shared instances API request {operation} {method} {path} failed with status {status}"
|
||||
))
|
||||
.into())
|
||||
);
|
||||
if status.is_server_error() {
|
||||
return Err(crate::ErrorKind::SharedInstancesApiError(message).into());
|
||||
}
|
||||
|
||||
Err(crate::ErrorKind::OtherError(message).into())
|
||||
}
|
||||
|
||||
pub(super) fn response_request_id(
|
||||
|
||||
@@ -244,13 +244,7 @@ pub(crate) async fn check_shared_instance_availability_before_launch(
|
||||
let availability =
|
||||
match get_remote_instance_access(&attachment.id, state).await {
|
||||
Ok(availability) => availability,
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.raw.as_ref(),
|
||||
crate::ErrorKind::NoCredentialsError
|
||||
| crate::ErrorKind::FetchError(_)
|
||||
) =>
|
||||
{
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
instance_id,
|
||||
shared_instance_id = %attachment.id,
|
||||
@@ -259,7 +253,6 @@ pub(crate) async fn check_shared_instance_availability_before_launch(
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
if let SharedInstanceRemoteResponse::Unavailable(reason) = availability {
|
||||
|
||||
@@ -124,6 +124,44 @@ pub async fn create_shared_instance_invite_link(
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_shared_instance_invites(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Vec<SharedInstanceInvite>> {
|
||||
let state = State::get().await?;
|
||||
let Some(attachment) = shared_attachment(instance_id, &state).await? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
ensure_owner(&attachment)?;
|
||||
|
||||
Ok(get_remote_invites(&attachment.id, &state)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|invite| SharedInstanceInvite {
|
||||
id: invite.id,
|
||||
expiration: invite.expiration,
|
||||
max_uses: invite.max_uses,
|
||||
uses: invite.uses,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(invite_id))]
|
||||
pub async fn revoke_shared_instance_invite(
|
||||
instance_id: &str,
|
||||
invite_id: String,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let _shared_instance_lock = state.lock_shared_instance(instance_id).await;
|
||||
let Some(attachment) = shared_attachment(instance_id, &state).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
ensure_owner(&attachment)?;
|
||||
|
||||
delete_remote_invite(&attachment.id, &invite_id, &state).await?;
|
||||
emit_instance(instance_id, InstancePayloadType::Edited).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_shared_instance_users(
|
||||
instance_id: &str,
|
||||
|
||||
@@ -110,8 +110,9 @@ pub use self::install::{
|
||||
};
|
||||
pub use self::invites::{
|
||||
accept_pending_shared_instance_invite, create_shared_instance_invite_link,
|
||||
decline_pending_shared_instance_invite, get_shared_instance_users,
|
||||
invite_shared_instance_users, remove_shared_instance_users,
|
||||
decline_pending_shared_instance_invite, get_shared_instance_invites,
|
||||
get_shared_instance_users, invite_shared_instance_users,
|
||||
remove_shared_instance_users, revoke_shared_instance_invite,
|
||||
};
|
||||
pub use self::publish::{
|
||||
get_shared_instance_publish_preview, publish_shared_instance,
|
||||
@@ -119,10 +120,11 @@ pub use self::publish::{
|
||||
};
|
||||
pub use self::types::{
|
||||
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
||||
SharedInstanceInviteInstallPreview, SharedInstanceInviteLink,
|
||||
SharedInstanceJoinType, SharedInstancePublishPreview,
|
||||
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
||||
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
||||
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
||||
SharedInstanceInviteLink, SharedInstanceJoinType,
|
||||
SharedInstancePublishPreview, SharedInstanceUpdateDiff,
|
||||
SharedInstanceUpdateDiffType, SharedInstanceUpdatePreview,
|
||||
SharedInstanceUser, SharedInstanceUsers,
|
||||
};
|
||||
|
||||
pub async fn can_active_user_use_shared_instances() -> crate::Result<bool> {
|
||||
|
||||
@@ -116,6 +116,15 @@ pub struct SharedInstanceInviteLink {
|
||||
pub max_uses: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SharedInstanceInvite {
|
||||
pub id: String,
|
||||
pub expiration: DateTime<Utc>,
|
||||
pub max_uses: i32,
|
||||
pub uses: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SharedInstanceInviteInstallPreview {
|
||||
|
||||
@@ -7,10 +7,7 @@ use crate::{
|
||||
install::{InstallPhaseDetails, InstallProgressReporter},
|
||||
prelude::ModLoader,
|
||||
state::{AppliedContentSetPatch, EditInstance, InstanceInstallStage},
|
||||
util::{
|
||||
fetch::{fetch, write_cached_icon},
|
||||
io,
|
||||
},
|
||||
util::{fetch::fetch, io},
|
||||
};
|
||||
|
||||
use super::{finish_import, recache_icon};
|
||||
@@ -90,18 +87,8 @@ pub async fn import_curseforge(
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
let filename = thumbnail_url.rsplit('/').next_back();
|
||||
if let Some(filename) = filename {
|
||||
icon = Some(
|
||||
write_cached_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir(),
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
icon =
|
||||
Some(crate::api::instance::cache_icon(icon_bytes, &state).await?);
|
||||
}
|
||||
|
||||
// base mod loader is always None for vanilla
|
||||
|
||||
@@ -346,19 +346,10 @@ pub async fn recache_icon(
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let bytes = tokio::fs::read(&icon_path).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
let bytes = bytes::Bytes::from(bytes);
|
||||
let cache_dir = &state.directories.caches_dir();
|
||||
let semaphore = &state.io_semaphore;
|
||||
if tokio::fs::try_exists(&icon_path).await.unwrap_or(false) {
|
||||
Ok(Some(
|
||||
fetch::write_cached_icon(
|
||||
&icon_path.to_string_lossy(),
|
||||
cache_dir,
|
||||
bytes,
|
||||
semaphore,
|
||||
)
|
||||
.await?,
|
||||
crate::api::instance::cache_icon_from_path(&icon_path, &state)
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
// could not find icon (for instance, prism default icon, etc)
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::state::{
|
||||
};
|
||||
use crate::util::fetch::{
|
||||
DownloadMeta, DownloadReason, FetchProgressFn, fetch,
|
||||
fetch_advanced_with_progress, sha1_file_async, write_cached_icon,
|
||||
fetch_advanced_with_progress, sha1_file_async,
|
||||
};
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use reqwest::Method;
|
||||
@@ -415,21 +415,7 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let filename = icon_url.rsplit('/').next();
|
||||
|
||||
if let Some(filename) = filename {
|
||||
Some(
|
||||
write_cached_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir(),
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(crate::api::instance::cache_icon(icon_bytes, &state).await?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user