feat(vpn-ui): scaffold rust backend (#4073)

* scaffold app

* feat: local storage for app data & config
This commit is contained in:
Pierre Dommerc
2023-11-13 12:33:58 +01:00
committed by GitHub
parent 11e01335c2
commit 2934d24e53
19 changed files with 463 additions and 25 deletions
+35
View File
@@ -25,6 +25,25 @@ yarn
yarn dev:app
```
or
```
cd src-tauri
cargo tauri dev
```
#### Logging
Rust logging (standard output) is controlled by the `RUST_LOG`
env variable
Example:
```
cd src-tauri
RUST_LOG=trace cargo tauri dev
```
## Dev in the browser
For convenience and better development experience, we can run the
@@ -43,6 +62,22 @@ When creating new tauri command, be sure to add the corresponding
mock definition into `nym-vpn/ui/src/dev/tauri-cmd-mocks/` and
update `nym-vpn/ui/src/dev/setup.ts` accordingly.
## Type bindings
[ts-rs](https://github.com/Aleph-Alpha/ts-rs) can be used to generate
TS type definitions from Rust types
To generate bindings, first
[annotate](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs)
Rust types, then run
```
cd src-tauri
cargo test
```
Generated TS types will be located in `src-tauri/bindings/`
## Build
To build as a **shared library**
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:app": "WEBKIT_DISABLE_COMPOSITING_MODE=1 tauri dev",
"dev:app": "RUST_LOG=nymvpn_ui=trace tauri dev",
"dev:browser": "vite --mode dev-browser",
"build": "tsc && vite build",
"build:app": "yarn build && cd src-tauri && cargo build --release --lib --features custom-protocol",
+1 -2
View File
@@ -1,4 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/bindings/
+67 -11
View File
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.21.0"
@@ -294,8 +300,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
@@ -570,6 +578,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dtoa"
version = "1.0.9"
@@ -599,7 +613,7 @@ checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2"
dependencies = [
"cc",
"rustc_version",
"toml 0.8.2",
"toml 0.8.5",
"vswhom",
"winreg",
]
@@ -1651,10 +1665,19 @@ dependencies = [
name = "nymvpn-ui"
version = "0.0.0"
dependencies = [
"anyhow",
"dotenvy",
"once_cell",
"serde",
"serde_json",
"tauri",
"tauri-build",
"thiserror",
"tokio",
"toml 0.8.5",
"tracing",
"tracing-subscriber",
"ts-rs",
]
[[package]]
@@ -2323,9 +2346,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
dependencies = [
"serde",
]
@@ -2557,7 +2580,7 @@ dependencies = [
"cfg-expr 0.15.5",
"heck 0.4.1",
"pkg-config",
"toml 0.8.2",
"toml 0.8.5",
"version-compare 0.1.1",
]
@@ -2847,6 +2870,15 @@ dependencies = [
"utf-8",
]
[[package]]
name = "termcolor"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
dependencies = [
"winapi-util",
]
[[package]]
name = "thin-slice"
version = "0.1.1"
@@ -2962,21 +2994,21 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.2"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
checksum = "3efaf127c78d5339cc547cce4e4d973bd5e4f56e949a06d091c082ebeef2f800"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.20.2",
"toml_edit 0.20.5",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
@@ -2996,9 +3028,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.20.2"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
checksum = "782bf6c2ddf761c1e7855405e8975472acf76f7f36d0d4328bd3b7a2fae12a85"
dependencies = [
"indexmap 2.0.2",
"serde",
@@ -3077,6 +3109,30 @@ dependencies = [
"serde_json",
]
[[package]]
name = "ts-rs"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1ff1f8c90369bc172200013ac17ae86e7b5def580687df4e6127883454ff2b0"
dependencies = [
"chrono",
"thiserror",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.38",
"termcolor",
]
[[package]]
name = "typenum"
version = "1.17.0"
+10 -1
View File
@@ -14,9 +14,18 @@ crate-type = ["cdylib"]
tauri-build = { version = "1.5", features = [] }
[dependencies]
tauri = { version = "1.5", features = ["shell-open"] }
tauri = { version = "1.5.2", features = ["shell-open"] }
tokio = { version = "1.33", features = ["rt", "sync", "time", "fs"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3.1", features = ["tracing-log", "env-filter"] }
anyhow = "1.0"
dotenvy = "0.15.7"
thiserror = "1.0"
ts-rs = { version = "7.0.0", features = ["chrono-impl"] }
once_cell = "1.18.0"
toml = "0.8.5"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
@@ -0,0 +1,61 @@
use std::time::Duration;
use tauri::State;
use tokio::time::sleep;
use tracing::{debug, instrument, trace};
use crate::{
error::CommandError,
states::{app::ConnectionState, SharedAppState},
};
#[instrument]
#[tauri::command]
pub async fn get_connection_state(
state: State<'_, SharedAppState>,
) -> Result<ConnectionState, CommandError> {
debug!("get_connection_state");
let app_state = state.lock().await;
Ok(app_state.state)
}
#[tauri::command]
pub async fn connect(state: State<'_, SharedAppState>) -> Result<ConnectionState, CommandError> {
debug!("connect");
let mut app_state = state.lock().await;
let ConnectionState::Disconnected = app_state.state else {
return Err(CommandError::CallerError(format!(
"cannot connect from state {:?}",
app_state.state
)));
};
// TODO fake some delay to establish connection
let app_state_cloned = state.inner().clone();
let task = tokio::spawn(async move {
sleep(Duration::from_secs(2)).await;
trace!("connected");
app_state_cloned.lock().await.state = ConnectionState::Connected;
});
let _ = task.await;
app_state.state = ConnectionState::Connecting;
Ok(app_state.state)
}
#[instrument]
#[tauri::command]
pub async fn disconnect(state: State<'_, SharedAppState>) -> Result<ConnectionState, CommandError> {
debug!("disconnect");
let mut app_state = state.lock().await;
let ConnectionState::Connected = app_state.state else {
return Err(CommandError::CallerError(format!(
"cannot disconnect from state {:?}",
app_state.state
)));
};
app_state.state = ConnectionState::Disconnecting;
Ok(app_state.state)
}
+11
View File
@@ -0,0 +1,11 @@
use tracing::{debug, instrument};
pub mod connection;
pub mod settings;
#[instrument]
#[tauri::command]
pub fn greet(name: &str) -> String {
debug!("greet");
format!("Hello, {}! You've been greeted from Rust!", name)
}
@@ -0,0 +1,32 @@
use tauri::State;
use tracing::{debug, instrument};
use crate::{error::CommandError, fs::data::AppData, states::SharedAppData};
#[instrument]
#[tauri::command]
pub async fn save_user_settings(state: State<'_, SharedAppData>) -> Result<(), CommandError> {
debug!("save_user_settings");
let app_data = state.lock().await;
app_data
.write()
.map_err(|e| CommandError::InternalError(e.to_string()))?;
Ok(())
}
#[instrument]
#[tauri::command]
pub async fn set_user_settings(
state: State<'_, SharedAppData>,
settings: AppData,
) -> Result<(), CommandError> {
debug!("set_user_settings");
let mut app_data = state.lock().await;
app_data.data = settings;
app_data
.write()
.map_err(|e| CommandError::InternalError(e.to_string()))?;
Ok(())
}
+12
View File
@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize)]
pub enum CommandError {
#[error("internal error: `{0}`")]
InternalError(String),
#[error("caller error: `{0}`")]
CallerError(String),
#[error("unknown error")]
Unknown,
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppCache {
// TODO
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppConfig {
// TODO
}
+13
View File
@@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
use crate::states::app::{NodeConfig, PrivacyMode};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppData {
pub monitoring: Option<bool>,
pub autoconnect: Option<bool>,
pub killswitch: Option<bool>,
pub privacy_mode: Option<PrivacyMode>,
pub entry_node: Option<NodeConfig>,
pub exit_node: Option<NodeConfig>,
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppLog {
// TODO
}
+4
View File
@@ -0,0 +1,4 @@
pub mod config;
pub mod data;
pub mod log;
pub mod storage;
+84
View File
@@ -0,0 +1,84 @@
use anyhow::{anyhow, Context, Result};
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt, fs, path::PathBuf, str};
use tauri::api::path::data_dir;
use tracing::{debug, error, instrument};
#[derive(Debug, Clone)]
pub struct AppStorage<T>
where
T: Serialize + DeserializeOwned + Default + fmt::Debug,
{
pub data: T,
pub dir_path: PathBuf,
pub filename: String,
pub full_path: PathBuf,
}
fn create_directory_path(path: &PathBuf) -> Result<()> {
let mut data_dir = data_dir().ok_or(anyhow!(
"Failed to retrieve data directory {:?}",
path.display()
))?;
data_dir.push(path);
fs::create_dir_all(&data_dir).context(format!(
"Failed to create data directory {}",
data_dir.display()
))
}
impl<T> AppStorage<T>
where
T: Serialize + DeserializeOwned + Default + fmt::Debug,
{
pub fn new(dir_path: PathBuf, filename: &str, data: Option<T>) -> Self {
let mut full_path = dir_path.clone();
full_path.push(filename);
Self {
data: data.unwrap_or_default(),
dir_path,
filename: filename.to_owned(),
full_path,
}
}
#[instrument]
pub fn read(&self) -> Result<T> {
// create the full directory path if it is missing
create_directory_path(&self.dir_path)?;
debug!("reading stored data from {}", self.full_path.display());
let content = fs::read(&self.full_path).context(format!(
"Failed to read data from {}",
self.full_path.display()
))?;
toml::from_str::<T>(str::from_utf8(&content)?).map_err(|e| {
error!("{e}");
anyhow!("{e}")
})
}
#[instrument]
pub fn write(&self) -> Result<()> {
// create the full directory path if it is missing
create_directory_path(&self.dir_path)?;
debug!("writing data to {}", self.full_path.display());
let toml = toml::to_string(&self.data)?;
fs::write(&self.full_path, toml)?;
Ok(())
}
#[instrument]
pub fn clear(&self) -> Result<()> {
// create the full directory path if it is missing
create_directory_path(&self.dir_path)?;
debug!("clearing data {}", self.full_path.display());
fs::write(&self.full_path, vec![])?;
Ok(())
}
}
+56 -7
View File
@@ -1,15 +1,64 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::Result;
use tauri::api::path::{config_dir, data_dir};
use tokio::sync::Mutex;
use tracing::info;
mod commands;
mod error;
mod fs;
mod states;
use commands::*;
use states::app::AppState;
use crate::fs::config::AppConfig;
use crate::fs::data::AppData;
use crate::fs::storage::AppStorage;
const APP_DIR: &str = "nymvpn";
const APP_DATA_FILE: &str = "app-data.toml";
const APP_CONFIG_FILE: &str = "config.toml";
fn main() -> Result<()> {
dotenvy::dotenv()?;
// uses RUST_LOG value for logging level
// eg. RUST_LOG=tauri=debug,nymvpn_ui=trace
tracing_subscriber::fmt::init();
let mut app_data_path = data_dir().ok_or(anyhow!("Failed to retrieve data directory"))?;
app_data_path.push(APP_DIR);
let app_data_store = AppStorage::<AppData>::new(app_data_path, APP_DATA_FILE, None);
let mut app_config_path = config_dir().ok_or(anyhow!("Failed to retrieve config directory"))?;
app_config_path.push(APP_DIR);
let app_config_store = AppStorage::<AppConfig>::new(app_config_path, APP_CONFIG_FILE, None);
info!("Starting tauri app");
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.manage(Arc::new(Mutex::new(AppState::default())))
.manage(Arc::new(Mutex::new(app_data_store)))
.manage(Arc::new(Mutex::new(app_config_store)))
.setup(|_app| {
info!("app setup");
Ok(())
})
.invoke_handler(tauri::generate_handler![
greet,
connection::get_connection_state,
connection::connect,
connection::disconnect,
settings::save_user_settings,
settings::set_user_settings,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Ok(())
}
+44
View File
@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NodeConfig {
pub id: String,
pub country: String,
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum ConnectionState {
Connected,
#[default]
Disconnected,
Connecting,
Disconnecting,
Error,
}
#[derive(Default, Debug, Serialize, Deserialize, TS, Clone)]
#[ts(export)]
pub enum PrivacyMode {
High,
Medium,
#[default]
Low,
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TunnelConfig {
pub id: String,
pub name: String,
}
#[derive(Debug, Default)]
pub struct AppState {
pub state: ConnectionState,
pub privacy_mode: PrivacyMode,
pub entry_node: Option<NodeConfig>,
pub exit_node: Option<NodeConfig>,
pub tunnel: Option<TunnelConfig>,
}
+11
View File
@@ -0,0 +1,11 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::fs::{config::AppConfig, data::AppData, storage::AppStorage};
pub mod app;
pub type SharedAppState = Arc<Mutex<app::AppState>>;
pub type SharedAppData = Arc<Mutex<AppStorage<AppData>>>;
pub type SharedAppConfig = Arc<Mutex<AppStorage<AppConfig>>>;
+3 -3
View File
@@ -29,7 +29,7 @@
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"identifier": "net.nymtech.vpn",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -46,8 +46,8 @@
"fullscreen": false,
"resizable": true,
"title": "NymVPN",
"width": 800,
"height": 600
"width": 200,
"height": 200
}
]
}