use std::borrow::Cow;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use tauri::utils::assets::{AssetKey, AssetsIter, CspHash};
use tauri::{App, Assets, Runtime};
use crate::runtime::Shared;
pub struct HotUpdateAssets<R: Runtime> {
embedded: Box<dyn Assets<R>>,
shared: Arc<Shared>,
}
impl<R: Runtime> HotUpdateAssets<R> {
pub(crate) fn new(embedded: Box<dyn Assets<R>>, shared: Arc<Shared>) -> Self {
Self { embedded, shared }
}
fn ota_file(&self, key: &AssetKey) -> Option<PathBuf> {
let dir = self.shared.active_dir()?;
let path = safe_join(dir, key.as_ref())?;
path.is_file().then_some(path)
}
}
impl<R: Runtime> Assets<R> for HotUpdateAssets<R> {
fn setup(&self, app: &App<R>) {
if !self.shared.is_activated() {
log::error!(
"hot-update: assets provider installed but the plugin never initialized — \
did you forget .plugin(tauri_plugin_hot_update::init(handle))? \
Serving embedded assets only"
);
}
self.embedded.setup(app);
}
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
if let Some(path) = self.ota_file(key) {
match std::fs::read(&path) {
Ok(bytes) => {
log::debug!("hot-update: {} served from OTA bundle", key.as_ref());
return Some(Cow::Owned(bytes));
}
Err(e) => {
log::warn!(
"hot-update: reading {} failed ({e}); falling back to embedded",
path.display()
);
}
}
}
self.embedded.get(key)
}
fn iter(&self) -> Box<AssetsIter<'_>> {
self.embedded.iter()
}
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
if self.ota_file(html_path).is_some() {
Box::new(std::iter::empty())
} else {
self.embedded.csp_hashes(html_path)
}
}
}
pub(crate) struct PlaceholderAssets;
impl<R: Runtime> Assets<R> for PlaceholderAssets {
fn get(&self, _key: &AssetKey) -> Option<Cow<'_, [u8]>> {
None
}
fn iter(&self) -> Box<AssetsIter<'_>> {
Box::new(std::iter::empty())
}
fn csp_hashes(&self, _html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
Box::new(std::iter::empty())
}
}
fn safe_join(dir: &Path, key: &str) -> Option<PathBuf> {
let rel = key.trim_start_matches('/');
if rel.is_empty() {
return None;
}
let mut out = dir.to_path_buf();
for component in Path::new(rel).components() {
match component {
Component::Normal(part) => out.push(part),
_ => return None,
}
}
Some(out)
}
#[cfg(test)]
mod tests;