Files
dioxus/packages/asset-resolver/src/lib.rs
T

191 lines
6.7 KiB
Rust

use http::{status::StatusCode, Response};
use std::path::{Path, PathBuf};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AssetServeError {
#[error("Failed to infer mime type for asset: {0}")]
InferringMimeType(std::io::Error),
#[error("Failed to serve asset: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to construct response: {0}")]
ResponseError(#[from] http::Error),
}
/// 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> {
// 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
///
/// Currently supports:
/// - [x] macOS
/// - [x] iOS
/// - [x] Windows
/// - [x] Linux (appimage)
/// - [ ] Linux (rpm)
/// - [ ] 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");
}
// 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 {
match trimmed.extension().and_then(|e| e.to_str()) {
// 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())
}
}