diff --git a/Cargo.lock b/Cargo.lock index 51e4e156..0c492ea1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,7 @@ dependencies = [ "openssl-sys", "rocket", "rocket_cors", + "rust-embed", "serde", "serde_json", "toml", @@ -366,6 +367,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "buf_redux" version = "0.8.4" @@ -524,6 +534,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -545,6 +564,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deranged" version = "0.3.8" @@ -584,6 +613,36 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "dyn-clone" version = "1.0.13" @@ -802,6 +861,16 @@ dependencies = [ "windows", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1424,7 +1493,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets", ] @@ -1590,6 +1659,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -1599,6 +1677,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "ref-cast" version = "1.0.20" @@ -1815,6 +1904,41 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.33", + "rust-embed-utils", + "shellexpand", + "syn 2.0.32", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1993,6 +2117,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -2012,6 +2147,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2127,7 +2271,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys", ] @@ -2399,6 +2543,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "ubyte" version = "0.10.3" diff --git a/aw-client-rust/tests/test.rs b/aw-client-rust/tests/test.rs index f149cdca..8fd21c1f 100644 --- a/aw-client-rust/tests/test.rs +++ b/aw-client-rust/tests/test.rs @@ -12,7 +12,6 @@ mod test { use aw_client_rust::Event; use chrono::{DateTime, Duration, Utc}; use serde_json::Map; - use std::path::PathBuf; use std::sync::Mutex; use std::thread; use tokio_test::block_on; @@ -38,10 +37,12 @@ mod test { } fn setup_testserver() -> rocket::Shutdown { + use aw_server::endpoints::AssetResolver; use aw_server::endpoints::ServerState; + let state = ServerState { datastore: Mutex::new(aw_datastore::Datastore::new_in_memory(false)), - asset_path: PathBuf::from("."), // webui won't be used, so it's invalidly set + asset_resolver: AssetResolver::new(None), device_id: "test_id".to_string(), }; let mut aw_config = aw_server::config::AWConfig::default(); diff --git a/aw-server/Cargo.toml b/aw-server/Cargo.toml index bea949d7..f1140278 100644 --- a/aw-server/Cargo.toml +++ b/aw-server/Cargo.toml @@ -29,6 +29,7 @@ gethostname = "0.4" uuid = { version = "1.3", features = ["serde", "v4"] } clap = { version = "4.1", features = ["derive", "cargo"] } log-panics = { version = "2", features = ["with-backtrace"]} +rust-embed = { version = "8.0.0", features = ["interpolate-folder-path"] } aw-datastore = { path = "../aw-datastore" } aw-models = { path = "../aw-models" } diff --git a/aw-server/build.rs b/aw-server/build.rs new file mode 100644 index 00000000..51414a6b --- /dev/null +++ b/aw-server/build.rs @@ -0,0 +1,8 @@ +use std::error::Error; + +fn main() -> Result<(), Box> { + std::fs::create_dir_all("../aw-webui/dist").unwrap(); + println!("cargo:rustc-env=AW_WEBUI_DIR=../aw-webui/dist"); + + Ok(()) +} diff --git a/aw-server/src/dirs.rs b/aw-server/src/dirs.rs index 2a6fdee3..b94456e9 100644 --- a/aw-server/src/dirs.rs +++ b/aw-server/src/dirs.rs @@ -1,5 +1,3 @@ -use std::env; -use std::ffi::OsString; use std::path::PathBuf; #[cfg(not(target_os = "android"))] @@ -93,104 +91,3 @@ fn test_get_dirs() { db_path(true).unwrap(); db_path(false).unwrap(); } - -// The appdirs implementation of site_data_dir is broken on computers which has flatpak installed -// as flatpak adds its ~/.local/share/flatpak/exports/share directory first and then doesn't care -// about the rest of the paths. -// This is a rewrite of site_data_dir which takes the first folder which exists out of all folders -// in XDG_DATA_DIRS -// TODO: Should we talk to upstream about this? This changes the behavior quite a lot so maybe they -// don't want this change? -fn site_data_dir(app: Option<&str>, _: Option<&str>) -> Result { - // Iterate over all XDG_DATA_DIRS and return first match that exists - let joined = match env::var_os("XDG_DATA_DIRS") { - // If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used. - // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - Some(path) => { - if path.is_empty() { - OsString::from("/usr/local/share:/usr/share") - } else { - path - } - } - None => OsString::from("/usr/local/share:/usr/share"), - }; - - for mut data_dir in env::split_paths(&joined) { - if let Some(app) = app { - data_dir.push(app); - } - if !data_dir.is_dir() { - continue; - } - return Ok(data_dir); - } - // If no dirs exists in XDG_DATA_DIRS, fallback to /usr/local/share - let default = "/usr/local/share"; - let mut data_dir = PathBuf::new(); - data_dir.push(default); - if let Some(app) = app { - data_dir.push(app); - } - - if data_dir.is_dir() { - Ok(data_dir) - } else { - Err(()) - } -} - -pub fn get_asset_path() -> PathBuf { - use std::env::current_exe; - - // Search order for asset path is: - // 1. ./aw-webui/dist - // 2. $current_exe_dir/aw_server_rust/static - // NOTE: Slightly different for .app bundles on macOS - // 3. $XDG_DATA_DIR/aw_server_rust/static - // 4. (fallback) ./aw-webui/dist - - // cargo_dev_path - // (for running with cargo run) - let cargo_dev_path = PathBuf::from("./aw-webui/dist/"); - if cargo_dev_path.as_path().exists() { - return cargo_dev_path; - } - - info!("Cannot find assets {:?}", cargo_dev_path.as_path()); - - // current_exe_path - // (for self-contained deployed binaries) - if let Ok(mut current_exe_path) = current_exe() { - current_exe_path.pop(); // remove name of executable - current_exe_path.push("./static/"); - if current_exe_path.as_path().exists() { - return current_exe_path; - } - } - - // For .app bundles on macOS - // - // On macOS, the executable location is ActivityWatch.app/Contents/MacOS/aw-server-rust, - // and the webui location is ActivityWatch.app/Contents/Resources/aw_server_rust/static. - if let Ok(mut current_exe_path) = current_exe() { - current_exe_path.pop(); // remove name of executable - current_exe_path.pop(); // step up into the Contents directory - current_exe_path.push("Resources/aw_server_rust/static/"); - if current_exe_path.as_path().exists() { - return current_exe_path; - } - } - - // usr_path - // (for linux usr installs) - if let Ok(mut usr_path) = site_data_dir(Some("aw-server"), None) { - usr_path.push("static"); - if usr_path.as_path().exists() { - return usr_path; - } - } - - warn!("Unable to find an aw-webui asset path which exists, falling back to ./aw-webui/dist"); - cargo_dev_path -} diff --git a/aw-server/src/endpoints/hostcheck.rs b/aw-server/src/endpoints/hostcheck.rs index 59a1097d..6f583cdf 100644 --- a/aw-server/src/endpoints/hostcheck.rs +++ b/aw-server/src/endpoints/hostcheck.rs @@ -114,7 +114,6 @@ impl Fairing for HostCheck { #[cfg(test)] mod tests { - use std::path::PathBuf; use std::sync::Mutex; use rocket::http::{ContentType, Header, Status}; @@ -126,7 +125,7 @@ mod tests { fn setup_testserver(address: String) -> Rocket { let state = endpoints::ServerState { datastore: Mutex::new(aw_datastore::Datastore::new_in_memory(false)), - asset_path: PathBuf::from("aw-webui/dist"), + asset_resolver: endpoints::AssetResolver::new(None), device_id: "test_id".to_string(), }; let mut aw_config = AWConfig::default(); diff --git a/aw-server/src/endpoints/mod.rs b/aw-server/src/endpoints/mod.rs index a080d2a0..7def3fa0 100644 --- a/aw-server/src/endpoints/mod.rs +++ b/aw-server/src/endpoints/mod.rs @@ -1,9 +1,11 @@ -use std::path::PathBuf; +use rust_embed::RustEmbed; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; use std::sync::Mutex; use gethostname::gethostname; use rocket::fs::FileServer; -use rocket::fs::NamedFile; +use rocket::http::ContentType; use rocket::serde::json::Json; use rocket::State; @@ -12,9 +14,33 @@ use crate::config::AWConfig; use aw_datastore::Datastore; use aw_models::Info; +#[derive(RustEmbed)] +#[folder = "$AW_WEBUI_DIR"] +struct EmbeddedAssets; + +pub struct AssetResolver { + asset_path: Option, +} + +impl AssetResolver { + pub fn new(asset_path: Option) -> Self { + Self { asset_path } + } + + fn resolve(&self, file_path: &str) -> Option> { + if let Some(asset_path) = &self.asset_path { + let content = std::fs::read(asset_path.join(file_path)); + if let Ok(data) = content { + return Some(data); + } + } + Some(EmbeddedAssets::get(file_path)?.data.to_vec()) + } +} + pub struct ServerState { pub datastore: Mutex, - pub asset_path: PathBuf, + pub asset_resolver: AssetResolver, pub device_id: String, } @@ -31,45 +57,33 @@ mod settings; pub use util::HttpErrorJson; #[get("/")] -async fn root_index(state: &State) -> Option { - NamedFile::open(state.asset_path.join("index.html")) - .await - .ok() +fn root_index(state: &State) -> Option<(ContentType, Vec)> { + get_file("index.html".into(), state) } #[get("/css/")] -async fn root_css(file: PathBuf, state: &State) -> Option { - NamedFile::open(state.asset_path.join("css").join(file)) - .await - .ok() +fn root_css(file: PathBuf, state: &State) -> Option<(ContentType, Vec)> { + get_file(Path::new("css").join(file), state) } #[get("/fonts/")] -async fn root_fonts(file: PathBuf, state: &State) -> Option { - NamedFile::open(state.asset_path.join("fonts").join(file)) - .await - .ok() +fn root_fonts(file: PathBuf, state: &State) -> Option<(ContentType, Vec)> { + get_file(Path::new("fonts").join(file), state) } #[get("/js/")] -async fn root_js(file: PathBuf, state: &State) -> Option { - NamedFile::open(state.asset_path.join("js").join(file)) - .await - .ok() +fn root_js(file: PathBuf, state: &State) -> Option<(ContentType, Vec)> { + get_file(Path::new("js").join(file), state) } #[get("/static/")] -async fn root_static(file: PathBuf, state: &State) -> Option { - NamedFile::open(state.asset_path.join("static").join(file)) - .await - .ok() +fn root_static(file: PathBuf, state: &State) -> Option<(ContentType, Vec)> { + get_file(Path::new("static").join(file), state) } #[get("/favicon.ico")] -async fn root_favicon(state: &State) -> Option { - NamedFile::open(state.asset_path.join("favicon.ico")) - .await - .ok() +fn root_favicon(state: &State) -> Option<(ContentType, Vec)> { + get_file("favicon.ico".into(), state) } #[get("/")] @@ -86,6 +100,18 @@ fn server_info(config: &State, state: &State) -> Json) -> Option<(ContentType, Vec)> { + let asset = state.asset_resolver.resolve(&file.display().to_string())?; + + let content_type = file + .extension() + .and_then(OsStr::to_str) + .and_then(ContentType::from_extension) + .unwrap_or(ContentType::Bytes); + + Some((content_type, asset)) +} + pub fn build_rocket(server_state: ServerState, config: AWConfig) -> rocket::Rocket { info!( "Starting aw-server-rust at {}:{}", @@ -156,3 +182,23 @@ pub fn build_rocket(server_state: ServerState, config: AWConfig) -> rocket::Rock } rocket } + +mod tests { + #[test] + fn test_filesystem_resolver() { + let resolver = super::AssetResolver::new(Some(".".into())); + + let content = resolver.resolve("Cargo.toml").unwrap(); + + assert!(String::from_utf8(content).unwrap().contains("aw-server")); + } + + #[test] + fn test_resolver_without_asset() { + let resolver = super::AssetResolver::new(Some(".".into())); + + let content = resolver.resolve("Cargo.json"); + + assert!(content.is_none()); + } +} diff --git a/aw-server/src/main.rs b/aw-server/src/main.rs index cc8558f0..73185a64 100644 --- a/aw-server/src/main.rs +++ b/aw-server/src/main.rs @@ -123,10 +123,7 @@ async fn main() -> Result<(), rocket::Error> { }; info!("Using DB at path {:?}", db_path); - let asset_path = match opts.webpath { - Some(webpath) => PathBuf::from(webpath), - None => dirs::get_asset_path(), - }; + let asset_path = opts.webpath.map(|webpath| PathBuf::from(webpath)); info!("Using aw-webui assets at path {:?}", asset_path); // Only use legacy import if opts.dbpath is not set @@ -145,7 +142,7 @@ async fn main() -> Result<(), rocket::Error> { // Even if legacy_import is set to true it is disabled on Android so // it will not happen there datastore: Mutex::new(aw_datastore::Datastore::new(db_path, legacy_import)), - asset_path, + asset_resolver: endpoints::AssetResolver::new(asset_path), device_id, }; diff --git a/aw-server/tests/api.rs b/aw-server/tests/api.rs index 18ecf67c..b0580038 100644 --- a/aw-server/tests/api.rs +++ b/aw-server/tests/api.rs @@ -8,7 +8,6 @@ extern crate aw_server; #[cfg(test)] mod api_tests { use std::collections::HashMap; - use std::path::PathBuf; use std::sync::Mutex; use chrono::{DateTime, Utc}; @@ -25,7 +24,7 @@ mod api_tests { fn setup_testserver() -> rocket::Rocket { let state = endpoints::ServerState { datastore: Mutex::new(aw_datastore::Datastore::new_in_memory(false)), - asset_path: PathBuf::from("aw-webui/dist"), + asset_resolver: endpoints::AssetResolver::new(None), device_id: "test_id".to_string(), }; let aw_config = config::AWConfig::default();