Cross platform asset resolver (#4521)

* add initial web resolver

* re-export resolver and add playwright test

* add documentation

* fix clippy

* fix docs + clippy
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-08-11 14:44:48 -07:00
1 parent b72b142b53
commit 68645e7f0c
16 files changed
+607 -203

No files matched your search

Generated
+10
View File
@@ -4966,6 +4966,7 @@ version = "0.7.0-alpha.3"
dependencies = [ dependencies = [
"criterion", "criterion",
"dioxus", "dioxus",
"dioxus-asset-resolver",
"dioxus-cli-config", "dioxus-cli-config",
"dioxus-config-macro", "dioxus-config-macro",
"dioxus-config-macros", "dioxus-config-macros",
@@ -5005,15 +5006,22 @@ dependencies = [
name = "dioxus-asset-resolver" name = "dioxus-asset-resolver"
version = "0.7.0-alpha.3" version = "0.7.0-alpha.3"
dependencies = [ dependencies = [
"dioxus",
"dioxus-cli-config", "dioxus-cli-config",
"http 1.3.1", "http 1.3.1",
"infer", "infer",
"jni 0.21.1", "jni 0.21.1",
"js-sys",
"manganis-core",
"ndk 0.9.0", "ndk 0.9.0",
"ndk-context", "ndk-context",
"ndk-sys 0.6.0+11769913", "ndk-sys 0.6.0+11769913",
"percent-encoding", "percent-encoding",
"serde_json",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio",
"wasm-bindgen-futures",
"web-sys",
] ]
[[package]] [[package]]
@@ -5223,6 +5231,8 @@ dependencies = [
"dioxus", "dioxus",
"flate2", "flate2",
"reqwest 0.12.22", "reqwest 0.12.22",
"serde",
"serde_json",
"tar", "tar",
] ]
+23 -5
View File
@@ -11,14 +11,32 @@ keywords = ["web", "desktop", "mobile", "gui", "wasm"]
rust-version = "1.79.0" rust-version = "1.79.0"
[dependencies] [dependencies]
http = { workspace = true }
percent-encoding = { workspace = true }
infer = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
dioxus-cli-config = { workspace = true } manganis-core = { workspace = true }
# native/fs resolver dependencies
http = { workspace = true, optional = true }
percent-encoding = { workspace = true, optional = true }
infer = { workspace = true, optional = true }
dioxus-cli-config = { workspace = true, optional = true }
tokio = { workspace = true, features = ["rt"], optional = true }
# browser resolver dependencies
web-sys = { workspace = true, features = ['Headers', 'Request', 'RequestInit', 'Response', 'Window'], optional = true }
js-sys = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
jni = "0.21.1" jni = { version = "0.21.1" }
ndk = { version = "0.9.0" } ndk = { version = "0.9.0" }
ndk-sys = { version = "0.6.0" } ndk-sys = { version = "0.6.0" }
ndk-context = { version = "0.1.1" } ndk-context = { version = "0.1.1" }
[dev-dependencies]
dioxus = { workspace = true }
serde_json = { workspace = true }
[features]
default = []
web = ["dep:web-sys", "dep:js-sys", "dep:wasm-bindgen-futures"]
native = ["dep:http", "dep:percent-encoding", "dep:infer", "dep:dioxus-cli-config", "dep:tokio"]
+3
View File
@@ -0,0 +1,3 @@
{
"key": "value"
}
+180 -193
View File
@@ -1,209 +1,196 @@
use http::{status::StatusCode, Response}; #![warn(missing_docs)]
use std::path::{Path, PathBuf}; //! The asset resolver for the Dioxus bundle format. Each platform has its own way of resolving assets. This crate handles
//! resolving assets in a cross-platform way.
//!
//! There are two broad locations for assets depending on the platform:
//! - **Web**: Assets are stored on a remote server and fetched via HTTP requests.
//! - **Native**: Assets are read from the local bundle. Each platform has its own bundle structure which may store assets
//! as a file at a specific path or in an opaque format like Android's AssetManager.
//!
//! [`read_asset_bytes`]( abstracts over both of these methods, allowing you to read the bytes of an asset
//! regardless of the platform.
//!
//! If you know you are on a desktop platform, you can use [`asset_path`] to resolve the path of an asset and read
//! the contents with [`std::fs`].
//!
//! ## Example
//! ```rust
//! # async fn asset_example() {
//! use dioxus::prelude::*;
//!
//! // Bundle the static JSON asset into the application
//! static JSON_ASSET: Asset = asset!("assets/data.json");
//!
//! // Read the bytes of the JSON asset
//! let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON_ASSET).await.unwrap();
//!
//! // Deserialize the JSON data
//! let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
//! assert_eq!(json["key"].as_str(), Some("value"));
//! # }
//! ```
use manganis_core::Asset;
use std::{fmt::Debug, path::PathBuf};
#[cfg(feature = "native")]
pub mod native;
#[cfg(feature = "web")]
mod web;
/// An error that can occur when resolving an asset to a path. Not all platforms can represent assets as paths,
/// an error may mean that the asset doesn't exist or it cannot be represented as a path.
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AssetServeError { pub enum AssetPathError {
#[error("Failed to infer mime type for asset: {0}")] /// The asset was not found by the resolver.
InferringMimeType(std::io::Error), #[error("Failed to find the path in the asset directory")]
NotFound,
#[error("Failed to serve asset: {0}")] /// The asset may exist, but it cannot be represented as a path.
#[error("Asset cannot be represented as a path")]
CannotRepresentAsPath,
}
/// Tries to resolve the path of an asset from a given URI path. Depending on the platform, this may
/// return an error even if the asset exists because some platforms cannot represent assets as paths.
/// You should prefer [`read_asset_bytes`] to read the asset bytes directly
/// for cross-platform compatibility.
///
/// ## Platform specific behavior
///
/// This function will only work on desktop platforms. It will always return an error in web and Android
/// bundles. On Android assets are bundled in the APK, and cannot be represented as paths. In web bundles,
/// Assets are fetched via HTTP requests and don't have a filesystem path.
///
/// ## Example
/// ```rust
/// use dioxus::prelude::*;
///
/// // Bundle the static JSON asset into the application
/// static JSON_ASSET: Asset = asset!("assets/data.json");
///
/// // Resolve the path of the asset. This will not work in web or Android bundles
/// let path = dioxus::asset_resolver::asset_path(&JSON_ASSET).unwrap();
///
/// println!("Asset path: {:?}", path);
///
/// // Read the bytes of the JSON asset
/// let bytes = std::fs::read(path).unwrap();
///
/// // Deserialize the JSON data
/// let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
/// assert_eq!(json["key"].as_str(), Some("value"));
/// ```
#[allow(unused)]
pub fn asset_path(asset: &Asset) -> Result<PathBuf, AssetPathError> {
#[cfg(all(feature = "web", target_arch = "wasm32"))]
return Err(AssetPathError::CannotRepresentAsPath);
#[cfg(feature = "native")]
return native::resolve_native_asset_path(asset.to_string().as_str());
Err(AssetPathError::NotFound)
}
/// An error that can occur when resolving an asset.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AssetResolveError {
/// An error occurred while resolving a native asset.
#[error("Failed to resolve native asset: {0}")]
Native(#[from] NativeAssetResolveError),
/// An error occurred while resolving a web asset.
#[error("Failed to resolve web asset: {0}")]
Web(#[from] WebAssetResolveError),
/// An error that occurs when no asset resolver is available for the current platform.
#[error("Asset resolution is not supported on this platform")]
UnsupportedPlatform,
}
/// Read the bytes of an asset. This will work on both web and native platforms. On the web,
/// it will fetch the asset via HTTP, and on native platforms, it will read the asset from the filesystem or bundle.
///
/// ## Errors
/// This function will return an error if the asset cannot be found or if it fails to read which may be due to I/O errors or
/// network issues.
///
/// ## Example
///
/// ```rust
/// # async fn asset_example() {
/// use dioxus::prelude::*;
///
/// // Bundle the static JSON asset into the application
/// static JSON_ASSET: Asset = asset!("assets/data.json");
///
/// // Read the bytes of the JSON asset
/// let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON_ASSET).await.unwrap();
///
/// // Deserialize the JSON data
/// let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
/// assert_eq!(json["key"].as_str(), Some("value"));
/// # }
/// ```
#[allow(unused)]
pub async fn read_asset_bytes(asset: &Asset) -> Result<Vec<u8>, AssetResolveError> {
let path = asset.to_string();
#[cfg(feature = "web")]
return web::resolve_web_asset(&path)
.await
.map_err(AssetResolveError::Web);
#[cfg(feature = "native")]
return tokio::task::spawn_blocking(move || native::resolve_native_asset(&path))
.await
.map_err(|err| AssetResolveError::Native(NativeAssetResolveError::JoinError(err)))
.and_then(|result| result.map_err(AssetResolveError::Native));
Err(AssetResolveError::UnsupportedPlatform)
}
/// An error that occurs when resolving a native asset.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum NativeAssetResolveError {
/// An I/O error occurred while reading the asset from the filesystem.
#[error("Failed to read asset: {0}")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("Failed to construct response: {0}")] /// The asset resolver failed to complete and could not be joined.
ResponseError(#[from] http::Error), #[cfg(feature = "native")]
#[error("Asset resolver join failed: {0}")]
JoinError(tokio::task::JoinError),
} }
/// Serve an asset from the filesystem or a custom asset handler. /// An error that occurs when resolving an asset on the web.
/// pub struct WebAssetResolveError {
/// This method properly accesses the asset directory based on the platform and serves the asset #[cfg(feature = "web")]
/// wrapped in an HTTP response. error: js_sys::Error,
///
/// Platform specifics:
/// - On the web, this returns AssetServerError since there's no filesystem access. Use `fetch` instead.
/// - On Android, it attempts to load assets using the Android AssetManager.
/// - On other platforms, it serves assets from the filesystem.
pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
// The path is the first part of the URI, so we need to trim the leading slash.
let mut uri_path = PathBuf::from(
percent_encoding::percent_decode_str(path)
.decode_utf8()
.expect("expected URL to be UTF-8 encoded")
.as_ref(),
);
// Attempt to serve from the asset dir on android using its loader
#[cfg(target_os = "android")]
{
if let Some(asset) = to_java_load_asset(path) {
return Ok(Response::builder()
.header("Content-Type", get_mime_by_ext(&uri_path))
.header("Access-Control-Allow-Origin", "*")
.body(asset)?);
}
}
// If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
// This lets us handle both absolute and relative paths without being too "special"
// It just means that our macos bundle is a little "special" because we need to place an `assets`
// dir in the `Resources` dir.
//
// If there's no asset root, we use the cargo manifest dir as the root, or the current dir
if !uri_path.exists() || uri_path.starts_with("/assets/") {
let bundle_root = get_asset_root();
let relative_path = uri_path.strip_prefix("/").unwrap();
uri_path = bundle_root.join(relative_path);
}
// If the asset exists, then we can serve it!
if uri_path.exists() {
let mime_type =
get_mime_from_path(&uri_path).map_err(AssetServeError::InferringMimeType)?;
let body = std::fs::read(uri_path)?;
return Ok(Response::builder()
.header("Content-Type", mime_type)
.header("Access-Control-Allow-Origin", "*")
.body(body)?);
}
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes())?)
} }
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach impl Debug for WebAssetResolveError {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// Currently supports: let mut debug = f.debug_struct("WebAssetResolveError");
/// - [x] macOS #[cfg(feature = "web")]
/// - [x] iOS debug.field("name", &self.error.name());
/// - [x] Windows #[cfg(feature = "web")]
/// - [x] Linux (appimage) debug.field("message", &self.error.message());
/// - [ ] Linux (rpm) debug.finish()
/// - [x] Linux (deb)
/// - [ ] Android
#[allow(unreachable_code)]
fn get_asset_root() -> PathBuf {
let cur_exe = std::env::current_exe().unwrap();
#[cfg(target_os = "macos")]
{
return cur_exe
.parent()
.unwrap()
.parent()
.unwrap()
.join("Resources");
}
#[cfg(target_os = "linux")]
{
// In linux bundles, the assets are placed in the lib/$product_name directory
// bin/
// main
// lib/
// $product_name/
// assets/
if let Some(product_name) = dioxus_cli_config::product_name() {
let lib_asset_path = || {
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
path.exists().then_some(path)
};
if let Some(asset_dir) = lib_asset_path() {
return asset_dir;
}
}
}
// For all others, the structure looks like this:
// app.(exe/appimage)
// main.exe
// assets/
cur_exe.parent().unwrap().to_path_buf()
}
/// Get the mime type from a path-like string
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
if asset.extension().is_some_and(|ext| ext == "svg") {
return Ok("image/svg+xml");
}
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
Some(f) if f != "text/plain" => Ok(f),
_other => Ok(get_mime_by_ext(asset)),
} }
} }
/// Get the mime type from a URI using its extension impl std::fmt::Display for WebAssetResolveError {
fn get_mime_by_ext(trimmed: &Path) -> &'static str { #[allow(unreachable_code)]
match trimmed.extension().and_then(|e| e.to_str()) { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// The common assets are all utf-8 encoded #[cfg(feature = "web")]
Some("js") => "text/javascript; charset=utf-8", return write!(f, "{}", self.error.message());
Some("css") => "text/css; charset=utf-8", write!(f, "WebAssetResolveError")
Some("json") => "application/json; charset=utf-8",
Some("svg") => "image/svg+xml; charset=utf-8",
Some("html") => "text/html; charset=utf-8",
// the rest... idk? probably not
Some("mjs") => "text/javascript; charset=utf-8",
Some("bin") => "application/octet-stream",
Some("csv") => "text/csv",
Some("ico") => "image/vnd.microsoft.icon",
Some("jsonld") => "application/ld+json",
Some("rtf") => "application/rtf",
Some("mp4") => "video/mp4",
// Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
Some(_) => "text/html; charset=utf-8",
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
// using octet stream according to this:
None => "application/octet-stream",
} }
} }
#[cfg(target_os = "android")] impl std::error::Error for WebAssetResolveError {}
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
let normalized = filepath
.trim_start_matches("/assets/")
.trim_start_matches('/');
// in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
#[cfg(debug_assertions)]
{
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
if path.exists() {
return std::fs::read(path).ok();
}
}
use std::ptr::NonNull;
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
// Query the Asset Manager
let asset_manager_ptr = env
.call_method(
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
"getAssets",
"()Landroid/content/res/AssetManager;",
&[],
)
.expect("Failed to get asset manager")
.l()
.expect("Failed to get asset manager as object");
unsafe {
let asset_manager =
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
let asset_manager = ndk::asset::AssetManager::from_ptr(
NonNull::new(asset_manager).expect("Invalid asset manager"),
);
let cstr = std::ffi::CString::new(normalized).unwrap();
let mut asset = asset_manager.open(&cstr)?;
Some(asset.buffer().unwrap().to_vec())
}
}
+281
View File
@@ -0,0 +1,281 @@
//! Native specific utilities for resolving assets in a bundle. This module is intended for use in renderers that
//! need to resolve asset bundles for resources like images, and fonts.
use http::{status::StatusCode, Response};
use std::path::{Path, PathBuf};
use crate::{AssetPathError, NativeAssetResolveError};
/// An error that can occur when serving an asset.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AssetServeError {
/// The asset path could not be resolved.
#[error("Failed to resolve asset: {0}")]
ResolveError(#[from] NativeAssetResolveError),
/// An error occurred while constructing the HTTP response.
#[error("Failed to construct response: {0}")]
ResponseError(#[from] http::Error),
}
/// Try to resolve the path of an asset from a given URI path.
pub(crate) fn resolve_native_asset_path(path: &str) -> Result<PathBuf, AssetPathError> {
#[allow(clippy::unnecessary_lazy_evaluations)]
resolve_asset_path_from_filesystem(path).ok_or_else(|| {
#[cfg(target_os = "android")]
{
// If the asset exists in the Android asset manager, return the can't be represented as a path
// error instead
if to_java_load_asset(path).is_some() {
return AssetPathError::CannotRepresentAsPath;
}
}
AssetPathError::NotFound
})
}
/// Try to resolve the path of an asset from a given URI path.
fn resolve_asset_path_from_filesystem(path: &str) -> Option<PathBuf> {
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
// The path is the first part of the URI, so we need to trim the leading slash.
let mut uri_path = PathBuf::from(
percent_encoding::percent_decode_str(path)
.decode_utf8()
.expect("expected URL to be UTF-8 encoded")
.as_ref(),
);
// If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
// This lets us handle both absolute and relative paths without being too "special"
// It just means that our macos bundle is a little "special" because we need to place an `assets`
// dir in the `Resources` dir.
//
// If there's no asset root, we use the cargo manifest dir as the root, or the current dir
if !uri_path.exists() || uri_path.starts_with("/assets/") {
let bundle_root = get_asset_root();
let relative_path = uri_path.strip_prefix("/").unwrap();
uri_path = bundle_root.join(relative_path);
}
// If the asset exists, return it
uri_path.exists().then_some(uri_path)
}
struct ResolvedAsset {
mime_type: &'static str,
body: Vec<u8>,
}
impl ResolvedAsset {
fn new(mime_type: &'static str, body: Vec<u8>) -> Self {
Self { mime_type, body }
}
fn into_response(self) -> Result<Response<Vec<u8>>, AssetServeError> {
Ok(Response::builder()
.header("Content-Type", self.mime_type)
.header("Access-Control-Allow-Origin", "*")
.body(self.body)?)
}
}
/// Read the bytes for an asset
pub(crate) fn resolve_native_asset(path: &str) -> Result<Vec<u8>, NativeAssetResolveError> {
// Attempt to serve from the asset dir on android using its loader
#[cfg(target_os = "android")]
{
if let Some(asset) = to_java_load_asset(path) {
return Ok(asset);
}
}
let Some(uri_path) = resolve_asset_path_from_filesystem(path) else {
return Err(NativeAssetResolveError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Asset not found",
)));
};
Ok(std::fs::read(uri_path)?)
}
/// Resolve the asset and its mime type
fn resolve_asset(path: &str) -> Result<Option<ResolvedAsset>, NativeAssetResolveError> {
// Attempt to serve from the asset dir on android using its loader
#[cfg(target_os = "android")]
{
if let Some(asset) = to_java_load_asset(path) {
let extension = path.rsplit_once('.').and_then(|(_, ext)| Some(ext));
let mime_type = get_mime_from_ext(extension);
return Ok(Some(ResolvedAsset::new(mime_type, asset)));
}
}
let Some(uri_path) = resolve_asset_path_from_filesystem(path) else {
return Ok(None);
};
let mime_type = get_mime_from_path(&uri_path)?;
let body = std::fs::read(uri_path)?;
Ok(Some(ResolvedAsset::new(mime_type, body)))
}
/// Serve an asset from the filesystem or a custom asset handler.
///
/// This method properly accesses the asset directory based on the platform and serves the asset
/// wrapped in an HTTP response.
///
/// Platform specifics:
/// - On the web, this returns AssetServerError since there's no filesystem access. Use `fetch` instead.
/// - On Android, it attempts to load assets using the Android AssetManager.
/// - On other platforms, it serves assets from the filesystem.
pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
match resolve_asset(path)? {
Some(asset) => asset.into_response(),
None => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes())?),
}
}
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
///
/// Currently supports:
/// - [x] macOS
/// - [x] iOS
/// - [x] Windows
/// - [x] Linux (appimage)
/// - [ ] Linux (rpm)
/// - [x] Linux (deb)
/// - [ ] Android
#[allow(unreachable_code)]
fn get_asset_root() -> PathBuf {
let cur_exe = std::env::current_exe().unwrap();
#[cfg(target_os = "macos")]
{
return cur_exe
.parent()
.unwrap()
.parent()
.unwrap()
.join("Resources");
}
#[cfg(target_os = "linux")]
{
// In linux bundles, the assets are placed in the lib/$product_name directory
// bin/
// main
// lib/
// $product_name/
// assets/
if let Some(product_name) = dioxus_cli_config::product_name() {
let lib_asset_path = || {
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
path.exists().then_some(path)
};
if let Some(asset_dir) = lib_asset_path() {
return asset_dir;
}
}
}
// For all others, the structure looks like this:
// app.(exe/appimage)
// main.exe
// assets/
cur_exe.parent().unwrap().to_path_buf()
}
/// Get the mime type from a path-like string
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
if asset.extension().is_some_and(|ext| ext == "svg") {
return Ok("image/svg+xml");
}
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
Some(f) if f != "text/plain" => Ok(f),
_other => Ok(get_mime_by_ext(asset)),
}
}
/// Get the mime type from a URI using its extension
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
let ext = trimmed.extension().as_ref().and_then(|ext| ext.to_str());
get_mime_from_ext(ext)
}
/// Get the mime type from a URI using its extension
fn get_mime_from_ext(ext: Option<&str>) -> &'static str {
match ext {
// The common assets are all utf-8 encoded
Some("js") => "text/javascript; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("json") => "application/json; charset=utf-8",
Some("svg") => "image/svg+xml; charset=utf-8",
Some("html") => "text/html; charset=utf-8",
// the rest... idk? probably not
Some("mjs") => "text/javascript; charset=utf-8",
Some("bin") => "application/octet-stream",
Some("csv") => "text/csv",
Some("ico") => "image/vnd.microsoft.icon",
Some("jsonld") => "application/ld+json",
Some("rtf") => "application/rtf",
Some("mp4") => "video/mp4",
// Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
Some(_) => "text/html; charset=utf-8",
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
// using octet stream according to this:
None => "application/octet-stream",
}
}
#[cfg(target_os = "android")]
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
let normalized = filepath
.trim_start_matches("/assets/")
.trim_start_matches('/');
// in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
#[cfg(debug_assertions)]
{
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
if path.exists() {
return std::fs::read(path).ok();
}
}
use std::ptr::NonNull;
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
// Query the Asset Manager
let asset_manager_ptr = env
.call_method(
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
"getAssets",
"()Landroid/content/res/AssetManager;",
&[],
)
.expect("Failed to get asset manager")
.l()
.expect("Failed to get asset manager as object");
unsafe {
let asset_manager =
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
let asset_manager = ndk::asset::AssetManager::from_ptr(
NonNull::new(asset_manager).expect("Invalid asset manager"),
);
let cstr = std::ffi::CString::new(normalized).unwrap();
let mut asset = asset_manager.open(&cstr)?;
Some(asset.buffer().unwrap().to_vec())
}
}
+50
View File
@@ -0,0 +1,50 @@
use js_sys::{
wasm_bindgen::{JsCast, JsValue},
ArrayBuffer, Uint8Array,
};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, Response};
use crate::WebAssetResolveError;
impl From<js_sys::Error> for WebAssetResolveError {
fn from(error: js_sys::Error) -> Self {
WebAssetResolveError { error }
}
}
impl WebAssetResolveError {
fn from_js_value(value: JsValue) -> Self {
if let Some(error) = value.dyn_ref::<js_sys::Error>() {
WebAssetResolveError::from(error.clone())
} else {
unreachable!("Expected a js_sys::Error, got: {:?}", value)
}
}
}
pub(crate) async fn resolve_web_asset(path: &str) -> Result<Vec<u8>, WebAssetResolveError> {
let url = if path.starts_with("/") {
path.to_string()
} else {
format!("/{path}")
};
let request = Request::new_with_str(&url).map_err(WebAssetResolveError::from_js_value)?;
let window = web_sys::window().unwrap();
let response_promise = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(WebAssetResolveError::from_js_value)?;
let response = response_promise.unchecked_into::<Response>();
let array_buffer_promise = response
.array_buffer()
.map_err(WebAssetResolveError::from_js_value)?;
let array_buffer: ArrayBuffer = JsFuture::from(array_buffer_promise)
.await
.map_err(WebAssetResolveError::from_js_value)?
.unchecked_into();
let bytes = Uint8Array::new(&array_buffer);
Ok(bytes.to_vec())
}
+1 -1
View File
@@ -17,7 +17,7 @@ dioxus-document = { workspace = true }
dioxus-signals = { workspace = true, optional = true } dioxus-signals = { workspace = true, optional = true }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
dioxus-cli-config = { workspace = true } dioxus-cli-config = { workspace = true }
dioxus-asset-resolver = { workspace = true } dioxus-asset-resolver = { workspace = true, features = ["native"] }
generational-box = { workspace = true } generational-box = { workspace = true }
dioxus-devtools = { workspace = true, optional = true } dioxus-devtools = { workspace = true, optional = true }
+1 -1
View File
@@ -66,7 +66,7 @@ pub(super) fn desktop_handler(
} }
} }
match dioxus_asset_resolver::serve_asset(request.uri().path()) { match dioxus_asset_resolver::native::serve_asset(request.uri().path()) {
Ok(res) => responder.respond(res), Ok(res) => responder.respond(res),
Err(_e) => responder.respond( Err(_e) => responder.respond(
Response::builder() Response::builder()
+3 -1
View File
@@ -30,6 +30,7 @@ dioxus-server = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true } dioxus-ssr = { workspace = true, optional = true }
dioxus-native = { workspace = true, optional = true } dioxus-native = { workspace = true, optional = true }
dioxus_server_macro = { workspace = true, optional = true } dioxus_server_macro = { workspace = true, optional = true }
dioxus-asset-resolver = { workspace = true, optional = true }
manganis = { workspace = true, features = ["dioxus"], optional = true } manganis = { workspace = true, features = ["dioxus"], optional = true }
dioxus-logger = { workspace = true, optional = true } dioxus-logger = { workspace = true, optional = true }
warnings = { workspace = true, optional = true } warnings = { workspace = true, optional = true }
@@ -69,7 +70,7 @@ hooks = ["dep:dioxus-hooks"]
devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/devtools"] devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/devtools"]
mounted = ["dioxus-web?/mounted"] mounted = ["dioxus-web?/mounted"]
file_engine = ["dioxus-web?/file_engine"] file_engine = ["dioxus-web?/file_engine"]
asset = ["dep:manganis"] asset = ["dep:manganis", "dep:dioxus-asset-resolver"]
document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"] document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
logger = ["dep:dioxus-logger"] logger = ["dep:dioxus-logger"]
cli-config = ["dep:dioxus-cli-config"] cli-config = ["dep:dioxus-cli-config"]
@@ -92,6 +93,7 @@ web = [
"dioxus-config-macro/web", "dioxus-config-macro/web",
"dep:dioxus-cli-config", "dep:dioxus-cli-config",
"dioxus-cli-config?/web", "dioxus-cli-config?/web",
"dioxus-asset-resolver?/web",
] ]
ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"] ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"] liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
+5
View File
@@ -129,6 +129,11 @@ pub use wasm_splitter as wasm_split;
pub use subsecond; pub use subsecond;
#[cfg(feature = "asset")]
#[cfg_attr(docsrs, doc(cfg(feature = "asset")))]
#[doc(inline)]
pub use dioxus_asset_resolver as asset_resolver;
pub mod prelude { pub mod prelude {
#[cfg(feature = "document")] #[cfg(feature = "document")]
#[cfg_attr(docsrs, doc(cfg(feature = "document")))] #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
+1 -1
View File
@@ -33,7 +33,7 @@ blitz-shell = { workspace = true }
dioxus-core = { workspace = true } dioxus-core = { workspace = true }
dioxus-html = { workspace = true } dioxus-html = { workspace = true }
dioxus-native-dom = { workspace = true } dioxus-native-dom = { workspace = true }
dioxus-asset-resolver = { workspace = true } dioxus-asset-resolver = { workspace = true, features = ["native"] }
dioxus-cli-config = { workspace = true, optional = true } dioxus-cli-config = { workspace = true, optional = true }
dioxus-devtools = { workspace = true, optional = true } dioxus-devtools = { workspace = true, optional = true }
dioxus-history = { workspace = true } dioxus-history = { workspace = true }
+1 -1
View File
@@ -33,7 +33,7 @@ impl NetProvider<Resource> for DioxusNativeNetProvider {
handler: blitz_traits::net::BoxedHandler<Resource>, handler: blitz_traits::net::BoxedHandler<Resource>,
) { ) {
if request.url.scheme() == "dioxus" { if request.url.scheme() == "dioxus" {
match dioxus_asset_resolver::serve_asset(request.url.path()) { match dioxus_asset_resolver::native::serve_asset(request.url.path()) {
Ok(res) => { Ok(res) => {
tracing::trace!("fetching asset from file system success {request:#?}"); tracing::trace!("fetching asset from file system success {request:#?}");
handler.bytes(doc_id, res.into_body().into(), self.callback.clone()) handler.bytes(doc_id, res.into_body().into(), self.callback.clone())
@@ -45,3 +45,15 @@ test("unused external assets are bundled", async ({ page }) => {
// make sure the response is an image // make sure the response is an image
expect(response.headers()["content-type"]).toBe("image/png"); expect(response.headers()["content-type"]).toBe("image/png");
}); });
test("assets are resolved", async ({ page }) => {
await page.goto("http://localhost:8989");
// Expect the page to contain an element with the id "resolved-data"
const resolvedData = page.locator("#resolved-data");
await expect(resolvedData).toBeVisible();
// Expect the element to contain the text "List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
await expect(resolvedData).toContainText(
"List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
);
});
@@ -8,6 +8,8 @@ publish = false
[dependencies] [dependencies]
dioxus = { workspace = true, features = ["web"] } dioxus = { workspace = true, features = ["web"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
[build-dependencies] [build-dependencies]
reqwest = { workspace = true, features = ["blocking"] } reqwest = { workspace = true, features = ["blocking"] }
@@ -0,0 +1,3 @@
{
"list": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
@@ -56,5 +56,36 @@ fn App() -> Element {
id: "some_image_without_hash", id: "some_image_without_hash",
src: "{SOME_IMAGE_WITHOUT_HASH}" src: "{SOME_IMAGE_WITHOUT_HASH}"
} }
LoadsAsset {}
}
}
const JSON: Asset = asset!("/assets/data.json");
#[derive(Debug, Clone, serde::Deserialize)]
struct Data {
list: Vec<i32>,
}
#[component]
fn LoadsAsset() -> Element {
let data = use_resource(|| async {
let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON)
.await
.unwrap();
serde_json::from_slice::<Data>(&bytes).unwrap()
});
match data() {
Some(data) => rsx! {
div {
id: "resolved-data",
"List: {data.list:?}"
}
},
None => rsx! {
div {
"Loading..."
}
},
} }
} }