Skip to content

Commit 7c6c7ad

Browse files
authored
feat(core): add asset_resolver API (#2879)
1 parent d13c48f commit 7c6c7ad

4 files changed

Lines changed: 100 additions & 59 deletions

File tree

.changes/asset-resolver.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Expose the `asset_resolver` API on the `App` and `AppHandle` structs.

core/tauri/src/app.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub(crate) mod tray;
88
use crate::{
99
command::{CommandArg, CommandItem},
1010
hooks::{InvokeHandler, OnPageLoad, PageLoadPayload, SetupHook},
11-
manager::{CustomProtocol, WindowManager},
11+
manager::{Asset, CustomProtocol, WindowManager},
1212
plugin::{Plugin, PluginStore},
1313
runtime::{
1414
http::{Request as HttpRequest, Response as HttpResponse},
@@ -169,6 +169,19 @@ impl PathResolver {
169169
}
170170
}
171171

172+
/// The asset resolver is a helper to access the [`tauri_utils::assets::Assets`] interface.
173+
#[derive(Debug, Clone)]
174+
pub struct AssetResolver<R: Runtime> {
175+
manager: WindowManager<R>,
176+
}
177+
178+
impl<R: Runtime> AssetResolver<R> {
179+
/// Gets the app asset associated with the given path.
180+
pub fn get(&self, path: String) -> Option<Asset> {
181+
self.manager.get_asset(path).ok()
182+
}
183+
}
184+
172185
/// A handle to the currently running application.
173186
///
174187
/// This type implements [`Manager`] which allows for manipulation of global application items.
@@ -400,6 +413,13 @@ macro_rules! shared_app_impl {
400413
pub fn package_info(&self) -> &PackageInfo {
401414
self.manager.package_info()
402415
}
416+
417+
/// The application's asset resolver.
418+
pub fn asset_resolver(&self) -> AssetResolver<R> {
419+
AssetResolver {
420+
manager: self.manager.clone(),
421+
}
422+
}
403423
}
404424
};
405425
}

core/tauri/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,14 @@ pub use {
8686
self::window::menu::MenuEvent,
8787
};
8888
pub use {
89-
self::app::{App, AppHandle, Builder, CloseRequestApi, Event, GlobalWindowEvent, PathResolver},
89+
self::app::{
90+
App, AppHandle, AssetResolver, Builder, CloseRequestApi, Event, GlobalWindowEvent, PathResolver,
91+
},
9092
self::hooks::{
9193
Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokeResolver, InvokeResponse, OnPageLoad,
9294
PageLoadPayload, SetupHook,
9395
},
96+
self::manager::Asset,
9497
self::runtime::{
9598
webview::{WebviewAttributes, WindowBuilder},
9699
window::{

core/tauri/src/manager.rs

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
9595
}
9696
}
9797

98+
/// A resolved asset.
99+
pub struct Asset {
100+
/// The asset bytes.
101+
pub bytes: Vec<u8>,
102+
/// The asset's mime type.
103+
pub mime_type: String,
104+
}
105+
98106
/// Uses a custom URI scheme handler to resolve file requests
99107
pub struct CustomProtocol<R: Runtime> {
100108
/// Handler for protocol
@@ -371,77 +379,82 @@ impl<R: Runtime> WindowManager<R> {
371379
})
372380
}
373381

382+
pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
383+
let assets = &self.inner.assets;
384+
if path.ends_with('/') {
385+
path.pop();
386+
}
387+
path = percent_encoding::percent_decode(path.as_bytes())
388+
.decode_utf8_lossy()
389+
.to_string();
390+
let path = if path.is_empty() {
391+
// if the url is `tauri://localhost`, we should load `index.html`
392+
"index.html".to_string()
393+
} else {
394+
// skip leading `/`
395+
path.chars().skip(1).collect::<String>()
396+
};
397+
let is_javascript = path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
398+
let is_html = path.ends_with(".html");
399+
400+
let asset_response = assets
401+
.get(&path.as_str().into())
402+
.or_else(|| assets.get(&format!("{}/index.html", path.as_str()).into()))
403+
.or_else(|| {
404+
#[cfg(debug_assertions)]
405+
eprintln!("Asset `{}` not found; fallback to index.html", path); // TODO log::error!
406+
assets.get(&"index.html".into())
407+
})
408+
.ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
409+
.map(Cow::into_owned);
410+
411+
match asset_response {
412+
Ok(asset) => {
413+
let final_data = match is_javascript || is_html {
414+
true => String::from_utf8_lossy(&asset)
415+
.into_owned()
416+
.replacen(
417+
"__TAURI__INVOKE_KEY_TOKEN__",
418+
&self.generate_invoke_key().to_string(),
419+
1,
420+
)
421+
.as_bytes()
422+
.to_vec(),
423+
false => asset,
424+
};
425+
let mime_type = MimeType::parse(&final_data, &path);
426+
Ok(Asset {
427+
bytes: final_data,
428+
mime_type,
429+
})
430+
}
431+
Err(e) => {
432+
#[cfg(debug_assertions)]
433+
eprintln!("{:?}", e); // TODO log::error!
434+
Err(Box::new(e))
435+
}
436+
}
437+
}
438+
374439
#[allow(clippy::type_complexity)]
375440
fn prepare_uri_scheme_protocol(
376441
&self,
377442
) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
378443
{
379-
let assets = self.inner.assets.clone();
380444
let manager = self.clone();
381445
Box::new(move |request| {
382-
let mut path = request
446+
let path = request
383447
.uri()
384448
.split(&['?', '#'][..])
385449
// ignore query string
386450
.next()
387451
.unwrap()
388452
.to_string()
389453
.replace("tauri://localhost", "");
390-
if path.ends_with('/') {
391-
path.pop();
392-
}
393-
path = percent_encoding::percent_decode(path.as_bytes())
394-
.decode_utf8_lossy()
395-
.to_string();
396-
let path = if path.is_empty() {
397-
// if the url is `tauri://localhost`, we should load `index.html`
398-
"index.html".to_string()
399-
} else {
400-
// skip leading `/`
401-
path.chars().skip(1).collect::<String>()
402-
};
403-
let is_javascript = path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
404-
let is_html = path.ends_with(".html");
405-
406-
let asset_response = assets
407-
.get(&path.as_str().into())
408-
.or_else(|| assets.get(&format!("{}/index.html", path.as_str()).into()))
409-
.or_else(|| {
410-
#[cfg(debug_assertions)]
411-
eprintln!("Asset `{}` not found; fallback to index.html", path); // TODO log::error!
412-
assets.get(&"index.html".into())
413-
})
414-
.ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
415-
.map(Cow::into_owned);
416-
417-
match asset_response {
418-
Ok(asset) => {
419-
let final_data = match is_javascript || is_html {
420-
true => String::from_utf8_lossy(&asset)
421-
.into_owned()
422-
.replacen(
423-
"__TAURI__INVOKE_KEY_TOKEN__",
424-
&manager.generate_invoke_key().to_string(),
425-
1,
426-
)
427-
.as_bytes()
428-
.to_vec(),
429-
false => asset,
430-
};
431-
432-
let mime_type = MimeType::parse(&final_data, &path);
433-
Ok(
434-
HttpResponseBuilder::new()
435-
.mimetype(&mime_type)
436-
.body(final_data)?,
437-
)
438-
}
439-
Err(e) => {
440-
#[cfg(debug_assertions)]
441-
eprintln!("{:?}", e); // TODO log::error!
442-
Err(Box::new(e))
443-
}
444-
}
454+
let asset = manager.get_asset(path)?;
455+
HttpResponseBuilder::new()
456+
.mimetype(&asset.mime_type)
457+
.body(asset.bytes)
445458
})
446459
}
447460

0 commit comments

Comments
 (0)