use alloc::{format, string::ToString};
use core::ops::Deref;
use std::path::Path;
use crate::{AssetError, AtomicWriteOutcome, download_remote_bytes, write_bytes_atomically};
pub struct LargeFile {
mmap: memmap2::Mmap,
size: usize,
}
impl core::fmt::Debug for LargeFile {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LargeFile")
.field("size", &self.size)
.finish_non_exhaustive()
}
}
impl LargeFile {
pub async fn from_local(path: impl AsRef<Path> + Send + 'static) -> Result<Self, AssetError> {
let path = path.as_ref().to_path_buf();
blocking::unblock(move || {
let file = std::fs::File::open(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
AssetError::not_found(path.display().to_string())
} else {
AssetError::mmap(path.display().to_string(), e.to_string())
}
})?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.map_err(|e| AssetError::mmap(path.display().to_string(), e.to_string()))?;
let size = mmap.len();
Ok(Self { mmap, size })
})
.await
}
pub async fn from_remote(url: &str) -> Result<Self, AssetError> {
let cache_path = download_to_cache(url).await?;
Self::from_local(cache_path).await
}
#[must_use]
pub const fn len(&self) -> usize {
self.size
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.size == 0
}
pub async fn warm(&self) {
let ptr_addr = self.mmap.as_ptr() as usize;
let len = self.mmap.len();
blocking::unblock(move || {
const PAGE_SIZE: usize = 4096;
let mut sum: u8 = 0;
for offset in (0..len).step_by(PAGE_SIZE) {
let ptr = ptr_addr as *const u8;
sum = sum.wrapping_add(unsafe { *ptr.add(offset) });
}
core::hint::black_box(sum);
})
.await;
}
}
impl Deref for LargeFile {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.mmap
}
}
impl AsRef<[u8]> for LargeFile {
fn as_ref(&self) -> &[u8] {
&self.mmap
}
}
async fn download_to_cache(url: &str) -> Result<std::path::PathBuf, AssetError> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(url.as_bytes());
let hash = hex::encode(hasher.finalize());
let cache_dir = dirs::cache_dir()
.map(|root| root.join("waterui").join("assets"))
.ok_or_else(|| AssetError::io("Could not determine cache directory"))?;
blocking::unblock({
let cache_dir = cache_dir.clone();
move || std::fs::create_dir_all(&cache_dir)
})
.await
.map_err(|e| AssetError::io(format!("Failed to create cache dir: {e}")))?;
let cache_path = cache_dir.join(&hash);
let cache_len = blocking::unblock({
let cache_path = cache_path.clone();
move || match std::fs::metadata(&cache_path) {
Ok(metadata) => Ok(Some(metadata.len())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
})
.await
.map_err(|e| AssetError::io(format!("Failed to read cached asset metadata: {e}")))?;
if let Some(cache_len) = cache_len {
if cache_len == 0 {
tracing::warn!(
"Ignoring empty cached asset for {url}: {}",
cache_path.display()
);
let _ = blocking::unblock({
let cache_path = cache_path.clone();
move || std::fs::remove_file(&cache_path)
})
.await;
} else {
tracing::debug!("Asset already cached: {url} -> {}", cache_path.display());
return Ok(cache_path);
}
}
tracing::info!("Downloading asset: {url}");
let bytes = download_remote_bytes(url).await?;
match write_bytes_atomically(&cache_path, &bytes).await? {
AtomicWriteOutcome::Written => {
tracing::debug!("Cached asset: {url} -> {}", cache_path.display());
}
AtomicWriteOutcome::ReusedExisting => {
tracing::debug!(
"Asset cache race detected, reusing {}",
cache_path.display()
);
}
}
Ok(cache_path)
}