use sha2::{Digest, Sha256};
#[inline]
fn normalize_cache_key(cloud_url: &str) -> &str {
match cloud_url.find('?') {
Some(idx) => &cloud_url[..idx],
None => cloud_url,
}
}
fn compute_url_hash(url: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(url.as_bytes());
hasher.finalize().iter().map(|b| format!("{:02x}", b)).collect()
}
#[derive(Debug, Clone)]
pub struct CacheStatistics {
pub cached_file_count: usize,
pub total_size_bytes: u64,
pub cache_directory: String,
}
impl std::fmt::Display for CacheStatistics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let size_mb = self.total_size_bytes as f64 / (1024.0 * 1024.0);
write!(
f,
"Cache: {} files, {:.2} MB in {}",
self.cached_file_count, size_mb, self.cache_directory
)
}
}
#[cfg(not(target_arch = "wasm32"))]
mod native_cache {
use std::fs;
use std::path::PathBuf;
use crate::ErrorManager::ErrorManager;
use super::{normalize_cache_key, compute_url_hash, CacheStatistics};
pub struct CloudFileCache {
cache_root_directory: PathBuf,
error_manager: ErrorManager,
}
impl CloudFileCache {
pub fn new(error_manager: ErrorManager) -> Self {
let cache_root = Self::get_cache_root_directory();
if let Err(e) = fs::create_dir_all(&cache_root) {
error_manager.log_warning(&format!("Failed to create cache directory: {}", e));
}
CloudFileCache { cache_root_directory: cache_root, error_manager }
}
fn get_cache_root_directory() -> PathBuf {
#[cfg(target_os = "windows")]
{
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
PathBuf::from(local_app_data).join("mdix_cache")
} else {
PathBuf::from(".mdix_cache")
}
}
#[cfg(not(target_os = "windows"))]
{
if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(".mdix_cache")
} else {
PathBuf::from(".mdix_cache")
}
}
}
pub fn get_cache_path(&self, cloud_url: &str) -> PathBuf {
let url_hash = compute_url_hash(normalize_cache_key(cloud_url));
let cache_subdir = &url_hash[..16];
let filename = Self::extract_filename_from_url(cloud_url);
self.cache_root_directory.join(cache_subdir).join(filename)
}
pub fn is_cached(&self, cloud_url: &str) -> bool {
self.get_cache_path(cloud_url).exists()
}
pub fn get_cached_content(&self, cloud_url: &str) -> Option<String> {
let cache_path = self.get_cache_path(cloud_url);
if !cache_path.exists() {
return None;
}
match fs::read_to_string(&cache_path) {
Ok(content) => {
self.error_manager.log_debug(&format!(
"Cache hit: {} -> {}",
cloud_url,
cache_path.display()
));
Some(content)
}
Err(e) => {
self.error_manager.log_warning(&format!(
"Failed to read cached file for '{}': {}",
cloud_url, e
));
None
}
}
}
pub fn cache_file(&self, cloud_url: &str, content: &str) {
let cache_path = self.get_cache_path(cloud_url);
if let Some(cache_dir) = cache_path.parent() {
if let Err(e) = fs::create_dir_all(cache_dir) {
self.error_manager
.log_warning(&format!("Failed to create cache directory: {}", e));
return;
}
}
match fs::write(&cache_path, content) {
Ok(_) => {
self.error_manager.log_debug(&format!(
"Cached: {} -> {}",
cloud_url,
cache_path.display()
));
}
Err(e) => {
self.error_manager.log_warning(&format!(
"Failed to cache file for '{}': {}",
cloud_url, e
));
}
}
}
pub fn prefetch(&self, cloud_url: &str, content: &str) {
self.cache_file(cloud_url, content);
}
pub fn clear_cache(&self) {
if self.cache_root_directory.exists() {
if let Err(e) = fs::remove_dir_all(&self.cache_root_directory) {
self.error_manager.log_warning(&format!("Failed to clear cache: {}", e));
} else {
let _ = fs::create_dir_all(&self.cache_root_directory);
}
}
}
pub fn get_statistics(&self) -> CacheStatistics {
if !self.cache_root_directory.exists() {
return CacheStatistics {
cached_file_count: 0,
total_size_bytes: 0,
cache_directory: self.cache_root_directory.display().to_string(),
};
}
let mut cached_file_count = 0usize;
let mut total_size_bytes = 0u64;
if let Ok(entries) = fs::read_dir(&self.cache_root_directory) {
for entry in entries.flatten() {
if let Ok(ft) = entry.file_type() {
if ft.is_file() {
cached_file_count += 1;
if let Ok(meta) = entry.metadata() {
total_size_bytes += meta.len();
}
} else if ft.is_dir() {
if let Ok(sub) = fs::read_dir(entry.path()) {
for sub_entry in sub.flatten() {
if sub_entry
.path()
.extension()
.and_then(|s| s.to_str())
== Some("mdix")
{
cached_file_count += 1;
if let Ok(meta) = sub_entry.metadata() {
total_size_bytes += meta.len();
}
}
}
}
}
}
}
}
CacheStatistics {
cached_file_count,
total_size_bytes,
cache_directory: self.cache_root_directory.display().to_string(),
}
}
fn extract_filename_from_url(url: &str) -> String {
if let Ok(parsed) = url::Url::parse(url) {
if let Some(mut segments) = parsed.path_segments() {
if let Some(last) = segments.next_back() {
if !last.is_empty() {
return last.to_string();
}
}
}
}
let hash = compute_url_hash(normalize_cache_key(url));
format!("import_{}.mdix", &hash[..8])
}
}
}
#[cfg(target_arch = "wasm32")]
mod wasm_cache {
use crate::ErrorManager::ErrorManager;
use super::{normalize_cache_key, compute_url_hash, CacheStatistics};
const KEY_PREFIX: &str = "mdix_cache:";
pub struct CloudFileCache {
error_manager: ErrorManager,
}
impl CloudFileCache {
pub fn new(error_manager: ErrorManager) -> Self {
if Self::storage().is_none() {
error_manager.log_warning(
"localStorage unavailable — cloud-import caching disabled for this session"
);
}
CloudFileCache { error_manager }
}
fn storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok()?
}
fn key_for(cloud_url: &str) -> String {
format!("{}{}", KEY_PREFIX, compute_url_hash(normalize_cache_key(cloud_url)))
}
pub fn is_cached(&self, cloud_url: &str) -> bool {
match Self::storage() {
Some(s) => matches!(s.get_item(&Self::key_for(cloud_url)), Ok(Some(_))),
None => false,
}
}
pub fn get_cached_content(&self, cloud_url: &str) -> Option<String> {
let storage = Self::storage()?;
match storage.get_item(&Self::key_for(cloud_url)) {
Ok(Some(content)) => {
self.error_manager.log_debug(&format!(
"Cache hit (localStorage): {}",
cloud_url
));
Some(content)
}
Ok(None) => None,
Err(_) => {
None
}
}
}
pub fn cache_file(&self, cloud_url: &str, content: &str) {
let Some(storage) = Self::storage() else {
self.error_manager.log_warning("localStorage unavailable — caching skipped");
return;
};
match storage.set_item(&Self::key_for(cloud_url), content) {
Ok(_) => {
self.error_manager.log_debug(&format!(
"Cached (localStorage): {}",
cloud_url
));
}
Err(_) => {
self.error_manager.log_warning(&format!(
"Failed to cache '{}' in localStorage (quota exceeded?)",
cloud_url
));
}
}
}
pub fn prefetch(&self, cloud_url: &str, content: &str) {
self.cache_file(cloud_url, content);
}
pub fn clear_cache(&self) {
let Some(storage) = Self::storage() else { return; };
let len = storage.length().unwrap_or(0);
let mut keys_to_remove = Vec::new();
for i in 0..len {
if let Ok(Some(key)) = storage.key(i) {
if key.starts_with(KEY_PREFIX) {
keys_to_remove.push(key);
}
}
}
for key in keys_to_remove {
let _ = storage.remove_item(&key);
}
}
pub fn get_statistics(&self) -> CacheStatistics {
let Some(storage) = Self::storage() else {
return CacheStatistics {
cached_file_count: 0,
total_size_bytes: 0,
cache_directory: "localStorage (unavailable)".to_string(),
};
};
let len = storage.length().unwrap_or(0);
let mut cached_file_count = 0usize;
let mut total_size_bytes = 0u64;
for i in 0..len {
if let Ok(Some(key)) = storage.key(i) {
if key.starts_with(KEY_PREFIX) {
cached_file_count += 1;
if let Ok(Some(val)) = storage.get_item(&key) {
total_size_bytes += val.len() as u64;
}
}
}
}
CacheStatistics {
cached_file_count,
total_size_bytes,
cache_directory: "localStorage".to_string(),
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use native_cache::CloudFileCache;
#[cfg(target_arch = "wasm32")]
pub use wasm_cache::CloudFileCache;