protocol.rs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. use std::{
  2. borrow::Cow,
  3. path::{Path, PathBuf},
  4. };
  5. use wry::{
  6. http::{status::StatusCode, Request, Response},
  7. webview::RequestAsyncResponder,
  8. Result,
  9. };
  10. use crate::desktop_context::EditQueue;
  11. static MINIFIED: &str = include_str!("./minified.js");
  12. fn module_loader(root_name: &str) -> String {
  13. format!(
  14. r#"
  15. <script type="module">
  16. {MINIFIED}
  17. // Wait for the page to load
  18. window.onload = function() {{
  19. let rootname = "{root_name}";
  20. let root_element = window.document.getElementById(rootname);
  21. if (root_element != null) {{
  22. window.interpreter.initialize(root_element);
  23. window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
  24. }}
  25. window.interpreter.wait_for_request();
  26. }}
  27. </script>
  28. "#
  29. )
  30. }
  31. pub(super) fn desktop_handler(
  32. request: &Request<Vec<u8>>,
  33. responder: RequestAsyncResponder,
  34. custom_head: Option<String>,
  35. custom_index: Option<String>,
  36. root_name: &str,
  37. edit_queue: &EditQueue,
  38. ) {
  39. // If the request is for the root, we'll serve the index.html file.
  40. if request.uri().path() == "/" {
  41. // If a custom index is provided, just defer to that, expecting the user to know what they're doing.
  42. // we'll look for the closing </body> tag and insert our little module loader there.
  43. let body = match custom_index {
  44. Some(custom_index) => custom_index
  45. .replace("</body>", &format!("{}</body>", module_loader(root_name)))
  46. .into_bytes(),
  47. None => {
  48. // Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
  49. let mut template = include_str!("./index.html").to_string();
  50. if let Some(custom_head) = custom_head {
  51. template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
  52. }
  53. template
  54. .replace("<!-- MODULE LOADER -->", &module_loader(root_name))
  55. .into_bytes()
  56. }
  57. };
  58. match Response::builder()
  59. .header("Content-Type", "text/html")
  60. .header("Access-Control-Allow-Origin", "*")
  61. .body(Cow::from(body))
  62. {
  63. Ok(response) => {
  64. responder.respond(response);
  65. return;
  66. }
  67. Err(err) => tracing::error!("error building response: {}", err),
  68. }
  69. } else if request.uri().path().trim_matches('/') == "edits" {
  70. edit_queue.handle_request(responder);
  71. return;
  72. }
  73. // Else, try to serve a file from the filesystem.
  74. let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
  75. .expect("expected URL to be UTF-8 encoded");
  76. let path = PathBuf::from(&*decoded);
  77. // If the path is relative, we'll try to serve it from the assets directory.
  78. let mut asset = get_asset_root()
  79. .unwrap_or_else(|| Path::new(".").to_path_buf())
  80. .join(&path);
  81. if !asset.exists() {
  82. asset = PathBuf::from("/").join(path);
  83. }
  84. if asset.exists() {
  85. let content_type = match get_mime_from_path(&asset) {
  86. Ok(content_type) => content_type,
  87. Err(err) => {
  88. tracing::error!("error getting mime type: {}", err);
  89. return;
  90. }
  91. };
  92. let asset = match std::fs::read(asset) {
  93. Ok(asset) => asset,
  94. Err(err) => {
  95. tracing::error!("error reading asset: {}", err);
  96. return;
  97. }
  98. };
  99. match Response::builder()
  100. .header("Content-Type", content_type)
  101. .body(Cow::from(asset))
  102. {
  103. Ok(response) => {
  104. responder.respond(response);
  105. return;
  106. }
  107. Err(err) => tracing::error!("error building response: {}", err),
  108. }
  109. }
  110. match Response::builder()
  111. .status(StatusCode::NOT_FOUND)
  112. .body(Cow::from(String::from("Not Found").into_bytes()))
  113. {
  114. Ok(response) => {
  115. responder.respond(response);
  116. }
  117. Err(err) => tracing::error!("error building response: {}", err),
  118. }
  119. }
  120. #[allow(unreachable_code)]
  121. fn get_asset_root() -> Option<PathBuf> {
  122. /*
  123. We're matching exactly how cargo-bundle works.
  124. - [x] macOS
  125. - [ ] Windows
  126. - [ ] Linux (rpm)
  127. - [ ] Linux (deb)
  128. - [ ] iOS
  129. - [ ] Android
  130. */
  131. if std::env::var_os("CARGO").is_some() {
  132. return None;
  133. }
  134. // TODO: support for other platforms
  135. #[cfg(target_os = "macos")]
  136. {
  137. let bundle = core_foundation::bundle::CFBundle::main_bundle();
  138. let bundle_path = bundle.path()?;
  139. let resources_path = bundle.resources_path()?;
  140. let absolute_resources_root = bundle_path.join(resources_path);
  141. let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
  142. return Some(canonical_resources_root);
  143. }
  144. None
  145. }
  146. /// Get the mime type from a path-like string
  147. fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
  148. if trimmed.extension().is_some_and(|ext| ext == "svg") {
  149. return Ok("image/svg+xml");
  150. }
  151. let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
  152. Some(f) => {
  153. if f == "text/plain" {
  154. get_mime_by_ext(trimmed)
  155. } else {
  156. f
  157. }
  158. }
  159. None => get_mime_by_ext(trimmed),
  160. };
  161. Ok(res)
  162. }
  163. /// Get the mime type from a URI using its extension
  164. fn get_mime_by_ext(trimmed: &Path) -> &'static str {
  165. match trimmed.extension().and_then(|e| e.to_str()) {
  166. Some("bin") => "application/octet-stream",
  167. Some("css") => "text/css",
  168. Some("csv") => "text/csv",
  169. Some("html") => "text/html",
  170. Some("ico") => "image/vnd.microsoft.icon",
  171. Some("js") => "text/javascript",
  172. Some("json") => "application/json",
  173. Some("jsonld") => "application/ld+json",
  174. Some("mjs") => "text/javascript",
  175. Some("rtf") => "application/rtf",
  176. Some("svg") => "image/svg+xml",
  177. Some("mp4") => "video/mp4",
  178. // Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
  179. Some(_) => "text/html",
  180. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
  181. // using octet stream according to this:
  182. None => "application/octet-stream",
  183. }
  184. }