mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 13:05:50 +00:00
feat: instance icon improvements
This commit is contained in:
@@ -49,6 +49,7 @@ hickory-resolver = { workspace = true }
|
||||
httpdate = { workspace = true }
|
||||
indicatif = { workspace = true, optional = true }
|
||||
itertools = { workspace = true }
|
||||
image = { workspace = true, features = ["gif", "jpeg", "png", "webp"] }
|
||||
modrinth-content-management = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
notify-debouncer-mini = { workspace = true }
|
||||
@@ -74,6 +75,7 @@ reqwest = { workspace = true, features = [
|
||||
"rustls-tls-webpki-roots",
|
||||
"stream",
|
||||
] }
|
||||
resvg = { workspace = true }
|
||||
rgb = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_ini = { workspace = true }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
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;
|
||||
const INSTANCE_ICON_MAX_SVG_SOURCE_BYTES: u64 = 16 * 1024 * 1024;
|
||||
|
||||
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) {
|
||||
normalize_svg(&bytes, None)
|
||||
} else {
|
||||
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 || {
|
||||
if has_svg_extension(&icon_path) {
|
||||
normalize_svg_from_path(&icon_path)
|
||||
} else {
|
||||
let file = StdFile::open(&icon_path).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not open instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
normalize_raster(BufReader::new(file))
|
||||
}
|
||||
})
|
||||
.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 metadata = match io::metadata(icon_path).await {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
instance_id = instance.id,
|
||||
icon_path,
|
||||
error = %error,
|
||||
"Failed to inspect legacy instance icon"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if metadata.len() <= INSTANCE_ICON_MAX_BYTES as u64 {
|
||||
continue;
|
||||
}
|
||||
|
||||
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 oversized legacy 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 normalize_svg_from_path(icon_path: &Path) -> crate::Result<Bytes> {
|
||||
let metadata = std::fs::metadata(icon_path).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not inspect instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
if metadata.len() > INSTANCE_ICON_MAX_SVG_SOURCE_BYTES {
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"SVG instance icons cannot exceed {} bytes before normalization",
|
||||
INSTANCE_ICON_MAX_SVG_SOURCE_BYTES
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let bytes = std::fs::read(icon_path).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not read instance icon {}: {error}",
|
||||
icon_path.display()
|
||||
))
|
||||
})?;
|
||||
normalize_svg(&bytes, icon_path.parent())
|
||||
}
|
||||
|
||||
fn normalize_svg(
|
||||
bytes: &[u8],
|
||||
resources_dir: Option<&Path>,
|
||||
) -> crate::Result<Bytes> {
|
||||
if bytes.len() as u64 > INSTANCE_ICON_MAX_SVG_SOURCE_BYTES {
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"SVG instance icons cannot exceed {} bytes before normalization",
|
||||
INSTANCE_ICON_MAX_SVG_SOURCE_BYTES
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut options = resvg::usvg::Options::default();
|
||||
options.resources_dir = resources_dir.map(Path::to_path_buf);
|
||||
options.fontdb_mut().load_system_fonts();
|
||||
let tree =
|
||||
resvg::usvg::Tree::from_data(bytes, &options).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not decode SVG instance icon: {error}"
|
||||
))
|
||||
})?;
|
||||
let size = tree.size();
|
||||
let scale = (INSTANCE_ICON_MAX_DIMENSION as f32
|
||||
/ size.width().max(size.height()))
|
||||
.min(1.0);
|
||||
let width = (size.width() * scale).ceil().max(1.0) as u32;
|
||||
let height = (size.height() * scale).ceil().max(1.0) as u32;
|
||||
let mut pixmap =
|
||||
resvg::tiny_skia::Pixmap::new(width, height).ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(
|
||||
"Could not allocate SVG instance icon output".to_string(),
|
||||
)
|
||||
})?;
|
||||
resvg::render(
|
||||
&tree,
|
||||
resvg::tiny_skia::Transform::from_scale(scale, scale),
|
||||
&mut pixmap.as_mut(),
|
||||
);
|
||||
let normalized = pixmap.encode_png().map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not encode SVG instance icon as PNG: {error}"
|
||||
))
|
||||
})?;
|
||||
|
||||
validate_normalized_icon(normalized)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 cannot exceed {} bytes",
|
||||
INSTANCE_ICON_MAX_BYTES
|
||||
))
|
||||
.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?;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::state::{
|
||||
InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel,
|
||||
State,
|
||||
};
|
||||
use crate::util::fetch::{self, write_cached_icon};
|
||||
use crate::util::fetch;
|
||||
use crate::util::io;
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -174,10 +174,8 @@ async fn resolve_icon_path(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let (bytes, file_name) = if icon.starts_with("https://")
|
||||
|| icon.starts_with("http://")
|
||||
{
|
||||
let fetched = fetch::fetch(
|
||||
let file = if icon.starts_with("https://") || icon.starts_with("http://") {
|
||||
let bytes = fetch::fetch(
|
||||
icon,
|
||||
None,
|
||||
None,
|
||||
@@ -186,21 +184,15 @@ async fn resolve_icon_path(
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
let name = icon.rsplit('/').next().unwrap_or("icon").to_string();
|
||||
(fetched, name)
|
||||
crate::api::instance::cache_icon(bytes, state).await?
|
||||
} else {
|
||||
let data = io::read(state.directories.caches_dir().join(icon)).await?;
|
||||
(bytes::Bytes::from(data), icon.to_string())
|
||||
crate::api::instance::cache_icon_from_path(
|
||||
&state.directories.caches_dir().join(icon),
|
||||
state,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let file = write_cached_icon(
|
||||
&file_name,
|
||||
&state.directories.caches_dir(),
|
||||
bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some(file.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,10 @@ impl State {
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = crate::api::instance::migrate_legacy_icons().await {
|
||||
tracing::error!("Error migrating legacy instance icons: {e}");
|
||||
}
|
||||
|
||||
let res = tokio::try_join!(
|
||||
state.discord_rpc.clear_to_default(true),
|
||||
instances::refresh_all_instances(),
|
||||
|
||||
@@ -14,10 +14,9 @@ use reqwest::Method;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::ffi::OsStr;
|
||||
use std::future::Future;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::{self, Duration, Instant, SystemTime};
|
||||
@@ -907,33 +906,6 @@ pub async fn copy(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Writes a icon to the cache and returns the absolute path of the icon within the cache directory
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn write_cached_icon(
|
||||
icon_path: &str,
|
||||
cache_dir: &Path,
|
||||
bytes: Bytes,
|
||||
semaphore: &IoSemaphore,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let hash = sha1_async(bytes.clone()).await?;
|
||||
let path = cache_dir
|
||||
.join("icons")
|
||||
.join(cached_icon_file_name(icon_path, &hash));
|
||||
|
||||
write(&path, &bytes, semaphore).await?;
|
||||
|
||||
let path = io::canonicalize(path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn cached_icon_file_name(icon_path: &str, hash: &str) -> String {
|
||||
let path = icon_path.split(['?', '#']).next().unwrap_or(icon_path);
|
||||
match Path::new(path).extension().and_then(OsStr::to_str) {
|
||||
Some(extension) => format!("{hash}.{extension}"),
|
||||
None => hash.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sha1_async(bytes: Bytes) -> crate::Result<String> {
|
||||
let hash = tokio::task::spawn_blocking(move || {
|
||||
sha1_smol::Sha1::from(bytes).hexdigest()
|
||||
|
||||
Reference in New Issue
Block a user