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), } pub fn serve_asset_from_raw_path(path: &str) -> Result>, 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( urlencoding::decode(path) .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(unused)] 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> { 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()) } }