refactor(core): add setup() to the Assets trait (#9147)

* feat(core): allow swapping the assets implemenetation

* refactor(core): add setup() to the Assets trait

* code review
This commit is contained in:
Lucas Fernandes Nogueira 2024-03-11 13:38:32 -03:00 committed by GitHub
parent 85de230f31
commit 490a6b424e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 101 additions and 60 deletions

5
.changes/assets-setup.md Normal file
View File

@ -0,0 +1,5 @@
---
"tauri": patch:feat
---
The `Assets` trait now include a `setup` method that lets you run initialization code for your custom asset provider.

View File

@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
The `Context` struct and the `Assets` trait now takes a `R: Runtime` generic.

View File

@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
Removed `Context::assets_mut` and added `Context::set_assets`.

View File

@ -0,0 +1,5 @@
---
"tauri-utils": patch:breaking
---
Removed the `assets::Assets` trait which is now part of the `tauri` crate.

View File

@ -345,10 +345,10 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
let info_plist = quote!(()); let info_plist = quote!(());
let pattern = match &options.pattern { let pattern = match &options.pattern {
PatternKind::Brownfield => quote!(#root::Pattern::Brownfield(std::marker::PhantomData)), PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
#[cfg(not(feature = "isolation"))] #[cfg(not(feature = "isolation"))]
PatternKind::Isolation { dir: _ } => { PatternKind::Isolation { dir: _ } => {
quote!(#root::Pattern::Brownfield(std::marker::PhantomData)) quote!(#root::Pattern::Brownfield)
} }
#[cfg(feature = "isolation")] #[cfg(feature = "isolation")]
PatternKind::Isolation { dir } => { PatternKind::Isolation { dir } => {

View File

@ -30,7 +30,7 @@ runtime = [ ]
[dependencies] [dependencies]
anyhow = { version = "1", optional = true } anyhow = { version = "1", optional = true }
serde = { version = "1", optional = true } serde = { version = "1", optional = true }
tauri-utils = { version = "2.0.0-beta.8", default-features = false, path = "../tauri-utils" } tauri-utils = { version = "2.0.0-beta.8", default-features = false, features = [ "build" ], path = "../tauri-utils" }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
glob = { version = "0.3", optional = true } glob = { version = "0.3", optional = true }
toml = { version = "0.8", optional = true } toml = { version = "0.8", optional = true }

View File

@ -104,18 +104,6 @@ impl CspHash<'_> {
} }
} }
/// Represents a container of file assets that are retrievable during runtime.
pub trait Assets: Send + Sync + 'static {
/// Get the content of the passed [`AssetKey`].
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>>;
/// Iterator for the assets.
fn iter(&self) -> Box<dyn Iterator<Item = (&&str, &&[u8])> + '_>;
/// Gets the hashes for the CSP tag of the HTML on the given path.
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_>;
}
/// [`Assets`] implementation that only contains compile-time compressed and embedded assets. /// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
#[derive(Debug)] #[derive(Debug)]
pub struct EmbeddedAssets { pub struct EmbeddedAssets {
@ -139,11 +127,10 @@ impl EmbeddedAssets {
html_hashes, html_hashes,
} }
} }
}
impl Assets for EmbeddedAssets { /// Get an asset by key.
#[cfg(feature = "compression")] #[cfg(feature = "compression")]
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> { pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
self self
.assets .assets
.get(key.as_ref()) .get(key.as_ref())
@ -157,8 +144,9 @@ impl Assets for EmbeddedAssets {
.map(Cow::Owned) .map(Cow::Owned)
} }
/// Get an asset by key.
#[cfg(not(feature = "compression"))] #[cfg(not(feature = "compression"))]
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> { pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
self self
.assets .assets
.get(key.as_ref()) .get(key.as_ref())
@ -166,11 +154,13 @@ impl Assets for EmbeddedAssets {
.map(|a| Cow::Owned(a.to_vec())) .map(|a| Cow::Owned(a.to_vec()))
} }
fn iter(&self) -> Box<dyn Iterator<Item = (&&str, &&[u8])> + '_> { /// Iterate on the assets.
Box::new(self.assets.into_iter()) pub fn iter(&self) -> Box<dyn Iterator<Item = (&str, &[u8])> + '_> {
Box::new(self.assets.into_iter().map(|(k, b)| (*k, *b)))
} }
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> { /// CSP hashes for the given asset.
pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
Box::new( Box::new(
self self
.global_hashes .global_hashes

View File

@ -266,7 +266,7 @@ impl<R: Runtime> AssetResolver<R> {
} }
/// Iterate on all assets. /// Iterate on all assets.
pub fn iter(&self) -> Box<dyn Iterator<Item = (&&str, &&[u8])> + '_> { pub fn iter(&self) -> Box<dyn Iterator<Item = (&str, &[u8])> + '_> {
self.manager.assets.iter() self.manager.assets.iter()
} }
} }
@ -1581,7 +1581,7 @@ tauri::Builder::default()
feature = "tracing", feature = "tracing",
tracing::instrument(name = "app::build", skip_all) tracing::instrument(name = "app::build", skip_all)
)] )]
pub fn build(mut self, context: Context) -> crate::Result<App<R>> { pub fn build(mut self, context: Context<R>) -> crate::Result<App<R>> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if self.menu.is_none() && self.enable_macos_default_menu { if self.menu.is_none() && self.enable_macos_default_menu {
self.menu = Some(Box::new(|app_handle| { self.menu = Some(Box::new(|app_handle| {
@ -1749,7 +1749,7 @@ tauri::Builder::default()
} }
/// Runs the configured Tauri application. /// Runs the configured Tauri application.
pub fn run(self, context: Context) -> crate::Result<()> { pub fn run(self, context: Context<R>) -> crate::Result<()> {
self.build(context)?.run(|_, _| {}); self.build(context)?.run(|_, _| {});
Ok(()) Ok(())
} }
@ -1824,6 +1824,8 @@ fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
.build_internal(&window_labels, &webview_labels)?; .build_internal(&window_labels, &webview_labels)?;
} }
app.manager.assets.setup(app);
if let Some(setup) = app.setup.take() { if let Some(setup) = app.setup.take() {
(setup)(app).map_err(|e| crate::Error::Setup(e.into()))?; (setup)(app).map_err(|e| crate::Error::Setup(e.into()))?;
} }

View File

@ -192,10 +192,12 @@ pub type SyncTask = Box<dyn FnOnce() + Send>;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
borrow::Cow,
collections::HashMap, collections::HashMap,
fmt::{self, Debug}, fmt::{self, Debug},
sync::MutexGuard, sync::MutexGuard,
}; };
use utils::assets::{AssetKey, CspHash, EmbeddedAssets};
#[cfg(feature = "wry")] #[cfg(feature = "wry")]
#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] #[cfg_attr(docsrs, doc(cfg(feature = "wry")))]
@ -224,7 +226,6 @@ pub use {
}, },
self::state::{State, StateManager}, self::state::{State, StateManager},
self::utils::{ self::utils::{
assets::Assets,
config::{Config, WebviewUrl}, config::{Config, WebviewUrl},
Env, PackageInfo, Theme, Env, PackageInfo, Theme,
}, },
@ -338,14 +339,47 @@ pub fn dev() -> bool {
!cfg!(feature = "custom-protocol") !cfg!(feature = "custom-protocol")
} }
/// Represents a container of file assets that are retrievable during runtime.
pub trait Assets<R: Runtime>: Send + Sync + 'static {
/// Initialize the asset provider.
fn setup(&self, app: &App<R>) {
let _ = app;
}
/// Get the content of the passed [`AssetKey`].
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>>;
/// Iterator for the assets.
fn iter(&self) -> Box<dyn Iterator<Item = (&str, &[u8])> + '_>;
/// Gets the hashes for the CSP tag of the HTML on the given path.
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_>;
}
impl<R: Runtime> Assets<R> for EmbeddedAssets {
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
EmbeddedAssets::get(self, key)
}
fn iter(&self) -> Box<dyn Iterator<Item = (&str, &[u8])> + '_> {
EmbeddedAssets::iter(self)
}
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
EmbeddedAssets::csp_hashes(self, html_path)
}
}
/// User supplied data required inside of a Tauri application. /// User supplied data required inside of a Tauri application.
/// ///
/// # Stability /// # Stability
/// This is the output of the [`generate_context`] macro, and is not considered part of the stable API. /// This is the output of the [`generate_context`] macro, and is not considered part of the stable API.
/// Unless you know what you are doing and are prepared for this type to have breaking changes, do not create it yourself. /// Unless you know what you are doing and are prepared for this type to have breaking changes, do not create it yourself.
pub struct Context { #[tauri_macros::default_runtime(Wry, wry)]
pub struct Context<R: Runtime> {
pub(crate) config: Config, pub(crate) config: Config,
pub(crate) assets: Box<dyn Assets>, /// Asset provider.
pub assets: Box<dyn Assets<R>>,
pub(crate) default_window_icon: Option<image::Image<'static>>, pub(crate) default_window_icon: Option<image::Image<'static>>,
pub(crate) app_icon: Option<Vec<u8>>, pub(crate) app_icon: Option<Vec<u8>>,
#[cfg(all(desktop, feature = "tray-icon"))] #[cfg(all(desktop, feature = "tray-icon"))]
@ -356,7 +390,7 @@ pub struct Context {
pub(crate) runtime_authority: RuntimeAuthority, pub(crate) runtime_authority: RuntimeAuthority,
} }
impl fmt::Debug for Context { impl<R: Runtime> fmt::Debug for Context<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = f.debug_struct("Context"); let mut d = f.debug_struct("Context");
d.field("config", &self.config) d.field("config", &self.config)
@ -372,7 +406,7 @@ impl fmt::Debug for Context {
} }
} }
impl Context { impl<R: Runtime> Context<R> {
/// The config the application was prepared with. /// The config the application was prepared with.
#[inline(always)] #[inline(always)]
pub fn config(&self) -> &Config { pub fn config(&self) -> &Config {
@ -387,14 +421,14 @@ impl Context {
/// The assets to be served directly by Tauri. /// The assets to be served directly by Tauri.
#[inline(always)] #[inline(always)]
pub fn assets(&self) -> &dyn Assets { pub fn assets(&self) -> &dyn Assets<R> {
self.assets.as_ref() self.assets.as_ref()
} }
/// A mutable reference to the assets to be served directly by Tauri. /// Replace the [`Assets`] implementation and returns the previous value so you can use it as a fallback if desired.
#[inline(always)] #[inline(always)]
pub fn assets_mut(&mut self) -> &mut Box<dyn Assets> { pub fn set_assets(&mut self, assets: Box<dyn Assets<R>>) -> Box<dyn Assets<R>> {
&mut self.assets std::mem::replace(&mut self.assets, assets)
} }
/// The default window icon Tauri should use when creating windows. /// The default window icon Tauri should use when creating windows.
@ -459,7 +493,7 @@ impl Context {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
config: Config, config: Config,
assets: Box<dyn Assets>, assets: Box<dyn Assets<R>>,
default_window_icon: Option<image::Image<'static>>, default_window_icon: Option<image::Image<'static>>,
app_icon: Option<Vec<u8>>, app_icon: Option<Vec<u8>>,
package_info: PackageInfo, package_info: PackageInfo,

View File

@ -24,8 +24,8 @@ use crate::{
event::{assert_event_name_is_valid, Event, EventId, EventTarget, Listeners}, event::{assert_event_name_is_valid, Event, EventId, EventTarget, Listeners},
ipc::{Invoke, InvokeHandler, InvokeResponder, RuntimeAuthority}, ipc::{Invoke, InvokeHandler, InvokeResponder, RuntimeAuthority},
plugin::PluginStore, plugin::PluginStore,
utils::{assets::Assets, config::Config, PackageInfo}, utils::{config::Config, PackageInfo},
Context, Pattern, Runtime, StateManager, Window, Assets, Context, Pattern, Runtime, StateManager, Window,
}; };
use crate::{event::EmitArgs, resources::ResourceTable, Webview}; use crate::{event::EmitArgs, resources::ResourceTable, Webview};
@ -48,7 +48,7 @@ struct CspHashStrings {
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]
pub(crate) fn set_csp<R: Runtime>( pub(crate) fn set_csp<R: Runtime>(
asset: &mut String, asset: &mut String,
assets: &impl std::borrow::Borrow<dyn Assets>, assets: &impl std::borrow::Borrow<dyn Assets<R>>,
asset_path: &AssetKey, asset_path: &AssetKey,
manager: &AppManager<R>, manager: &AppManager<R>,
csp: Csp, csp: Csp,
@ -179,7 +179,7 @@ pub struct AppManager<R: Runtime> {
pub listeners: Listeners, pub listeners: Listeners,
pub state: Arc<StateManager>, pub state: Arc<StateManager>,
pub config: Config, pub config: Config,
pub assets: Box<dyn Assets>, pub assets: Box<dyn Assets<R>>,
pub app_icon: Option<Vec<u8>>, pub app_icon: Option<Vec<u8>>,
@ -216,7 +216,7 @@ impl<R: Runtime> fmt::Debug for AppManager<R> {
impl<R: Runtime> AppManager<R> { impl<R: Runtime> AppManager<R> {
#[allow(clippy::too_many_arguments, clippy::type_complexity)] #[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn with_handlers( pub(crate) fn with_handlers(
#[allow(unused_mut)] mut context: Context, #[allow(unused_mut)] mut context: Context<R>,
plugins: PluginStore<R>, plugins: PluginStore<R>,
invoke_handler: Box<InvokeHandler<R>>, invoke_handler: Box<InvokeHandler<R>>,
on_page_load: Option<Arc<OnPageLoad<R>>>, on_page_load: Option<Arc<OnPageLoad<R>>>,

View File

@ -2,28 +2,25 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::marker::PhantomData;
#[cfg(feature = "isolation")] #[cfg(feature = "isolation")]
use std::sync::Arc; use std::sync::Arc;
use serde::Serialize; use serde::Serialize;
use serialize_to_javascript::{default_template, Template}; use serialize_to_javascript::{default_template, Template};
use tauri_utils::assets::{Assets, EmbeddedAssets};
/// The domain of the isolation iframe source. /// The domain of the isolation iframe source.
pub const ISOLATION_IFRAME_SRC_DOMAIN: &str = "localhost"; pub const ISOLATION_IFRAME_SRC_DOMAIN: &str = "localhost";
/// An application pattern. /// An application pattern.
#[derive(Debug)] #[derive(Debug)]
pub enum Pattern<A: Assets = EmbeddedAssets> { pub enum Pattern {
/// The brownfield pattern. /// The brownfield pattern.
Brownfield(PhantomData<A>), Brownfield,
/// Isolation pattern. Recommended for security purposes. /// Isolation pattern. Recommended for security purposes.
#[cfg(feature = "isolation")] #[cfg(feature = "isolation")]
Isolation { Isolation {
/// The HTML served on `isolation://index.html`. /// The HTML served on `isolation://index.html`.
assets: Arc<A>, assets: Arc<tauri_utils::assets::EmbeddedAssets>,
/// The schema used for the isolation frames. /// The schema used for the isolation frames.
schema: String, schema: String,
@ -55,7 +52,7 @@ pub(crate) enum PatternObject {
impl From<&Pattern> for PatternObject { impl From<&Pattern> for PatternObject {
fn from(pattern: &Pattern) -> Self { fn from(pattern: &Pattern) -> Self {
match pattern { match pattern {
Pattern::Brownfield(_) => Self::Brownfield, Pattern::Brownfield => Self::Brownfield,
#[cfg(feature = "isolation")] #[cfg(feature = "isolation")]
Pattern::Isolation { .. } => Self::Isolation { Pattern::Isolation { .. } => Self::Isolation {
side: IsolationSide::default(), side: IsolationSide::default(),

View File

@ -2,12 +2,10 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use crate::Assets;
use http::header::CONTENT_TYPE; use http::header::CONTENT_TYPE;
use serialize_to_javascript::Template; use serialize_to_javascript::Template;
use tauri_utils::{ use tauri_utils::{assets::EmbeddedAssets, config::Csp};
assets::{Assets, EmbeddedAssets},
config::Csp,
};
use std::sync::Arc; use std::sync::Arc;
@ -29,7 +27,7 @@ pub fn get<R: Runtime>(
format!("{schema}:") format!("{schema}:")
}; };
let assets = assets as Arc<dyn Assets>; let assets = assets as Arc<dyn Assets<R>>;
Box::new(move |request, responder| { Box::new(move |request, responder| {
let response = match request_to_path(&request).as_str() { let response = match request_to_path(&request).as_str() {

View File

@ -57,27 +57,27 @@ use std::{borrow::Cow, collections::HashMap, fmt::Debug};
use crate::{ use crate::{
ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority}, ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority},
webview::InvokeRequest, webview::InvokeRequest,
App, Builder, Context, Pattern, Webview, App, Assets, Builder, Context, Pattern, Runtime, Webview,
}; };
use tauri_utils::{ use tauri_utils::{
acl::resolved::Resolved, acl::resolved::Resolved,
assets::{AssetKey, Assets, CspHash}, assets::{AssetKey, CspHash},
config::{AppConfig, Config}, config::{AppConfig, Config},
}; };
/// An empty [`Assets`] implementation. /// An empty [`Assets`] implementation.
pub struct NoopAsset { pub struct NoopAsset {
assets: HashMap<&'static str, &'static [u8]>, assets: HashMap<String, Vec<u8>>,
csp_hashes: Vec<CspHash<'static>>, csp_hashes: Vec<CspHash<'static>>,
} }
impl Assets for NoopAsset { impl<R: Runtime> Assets<R> for NoopAsset {
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> { fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
None None
} }
fn iter(&self) -> Box<dyn Iterator<Item = (&&str, &&[u8])> + '_> { fn iter(&self) -> Box<dyn Iterator<Item = (&str, &[u8])> + '_> {
Box::new(self.assets.iter()) Box::new(self.assets.iter().map(|(k, b)| (k.as_str(), b.as_slice())))
} }
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> { fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
@ -94,7 +94,7 @@ pub fn noop_assets() -> NoopAsset {
} }
/// Creates a new [`crate::Context`] for testing. /// Creates a new [`crate::Context`] for testing.
pub fn mock_context<A: Assets>(assets: A) -> crate::Context { pub fn mock_context<R: Runtime, A: Assets<R>>(assets: A) -> crate::Context<R> {
Context { Context {
config: Config { config: Config {
schema: None, schema: None,
@ -125,7 +125,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context {
crate_name: "test", crate_name: "test",
}, },
_info_plist: (), _info_plist: (),
pattern: Pattern::Brownfield(std::marker::PhantomData), pattern: Pattern::Brownfield,
runtime_authority: RuntimeAuthority::new(Default::default(), Resolved::default()), runtime_authority: RuntimeAuthority::new(Default::default(), Resolved::default()),
} }
} }