OneLauncher-Cracked/packages/oneclient_core/src/images/mod.rs
2026-07-14 18:06:23 +02:00

128 lines
3.9 KiB
Rust

use std::collections::HashMap;
use std::path::PathBuf;
use bytes::Bytes;
use reqwest::Method;
use tokio::sync::Mutex;
use crate::crypto::sha1_bytes;
use crate::http::RequestError;
use crate::paths;
use crate::state::LauncherServices;
use crate::LauncherResult;
pub const DEFAULT_IMAGE_EDGE: u32 = 1600;
#[derive(Debug, Default)]
pub struct ImageCacheStore {
memory: Mutex<HashMap<String, Bytes>>,
}
impl ImageCacheStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[tracing::instrument(level = "debug", skip(self, services))]
pub async fn get(
&self,
services: &LauncherServices,
url: &str,
max_edge: u32,
) -> LauncherResult<Bytes> {
let mem_key = Self::mem_key(url, max_edge);
if let Some(hit) = self.memory.lock().await.get(&mem_key).cloned() {
return Ok(hit);
}
let path = Self::disk_path(url, max_edge)?;
if let Ok(bytes) = polyio::read(&path).await {
let bytes = Bytes::from(bytes);
self.memory.lock().await.insert(mem_key, bytes.clone());
return Ok(bytes);
}
let raw = download(services, url).await?;
let bytes = downscale_if_oversized(raw, max_edge).await;
if let Some(parent) = path.parent() {
polyio::create_dir_all(parent).await?;
}
if let Err(err) = polyio::write(&path, &bytes).await {
tracing::warn!("failed to persist cached image for {url}: {err}");
}
self.memory.lock().await.insert(mem_key, bytes.clone());
Ok(bytes)
}
fn mem_key(url: &str, max_edge: u32) -> String {
format!("{max_edge}|{url}")
}
fn disk_path(url: &str, max_edge: u32) -> LauncherResult<PathBuf> {
let mut name = sha1_bytes(url.as_bytes());
name.push('_');
name.push_str(&max_edge.to_string());
if let Some(ext) = extension_from_url(url) {
name.push('.');
name.push_str(&ext);
}
Ok(paths::images_cache_dir()?.join(name))
}
}
static DECODE_LIMIT: std::sync::LazyLock<tokio::sync::Semaphore> =
std::sync::LazyLock::new(|| tokio::sync::Semaphore::new(2));
async fn downscale_if_oversized(bytes: Bytes, max_edge: u32) -> Bytes {
let _permit = DECODE_LIMIT.acquire().await;
let candidate = bytes.clone();
match tokio::task::spawn_blocking(move || downscale(&candidate, max_edge)).await {
Ok(Some(smaller)) => smaller,
_ => bytes,
}
}
fn downscale(bytes: &[u8], max_edge: u32) -> Option<Bytes> {
let img = image::load_from_memory(bytes).ok()?;
if img.width().max(img.height()) <= max_edge {
return None;
}
let resized = img.resize(max_edge, max_edge, image::imageops::FilterType::Lanczos3);
let mut out = Vec::new();
let mut cursor = std::io::Cursor::new(&mut out);
if resized.color().has_alpha() {
resized.write_to(&mut cursor, image::ImageFormat::Png).ok()?;
} else {
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, 85)
.encode_image(&resized)
.ok()?;
}
Some(Bytes::from(out))
}
#[tracing::instrument(level = "debug", skip(services))]
async fn download(services: &LauncherServices, url: &str) -> LauncherResult<Bytes> {
let parsed = url.parse().map_err(RequestError::from)?;
let request = reqwest::Request::new(Method::GET, parsed);
let res = services.requester.send(request).await?;
let bytes = res.bytes().await.map_err(RequestError::from)?;
Ok(bytes)
}
fn extension_from_url(url: &str) -> Option<String> {
let path = url.split(['?', '#']).next().unwrap_or(url);
let name = path.rsplit('/').next().unwrap_or(path);
let ext = name.rsplit_once('.')?.1;
if ext.is_empty() || ext.len() > 5 || !ext.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
Some(ext.to_ascii_lowercase())
}