mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-01-06 09:50:10 -06:00
Add super basic WASM component management for first-party components (currently only auth-ui) to the CLI.
Something like this could involve into a plugin eco-system but that is a long way. Also this is not a package/dependency manager, there's no notion of transitivity etc.
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -7999,8 +7999,11 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"minijinja",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -8010,6 +8013,7 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"zip 5.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8529,7 +8533,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"url",
|
||||
"utoipa",
|
||||
"zip",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10060,6 +10064,20 @@ dependencies = [
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "5.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap 2.11.4",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.2"
|
||||
|
||||
@@ -75,7 +75,9 @@ askama = { version = "0.14.0", default-features = false, features = ["derive", "
|
||||
axum = { version = "^0.8.1", features = ["multipart"] }
|
||||
env_logger = { version = "^0.11.8", default-features = false, features = ["auto-color", "humantime"] }
|
||||
libsqlite3-sys = { version = "0.35.0", default-features = false, features = ["bundled", "preupdate_hook"] }
|
||||
minijinja = { version = "2.1.2", default-features = false }
|
||||
parking_lot = { version = "0.12.3", default-features = false, features = ["send_guard", "arc_lock"] }
|
||||
reqwest = { version = "0.12.8", default-features = false, features = ["rustls-tls", "json"] }
|
||||
rusqlite = { version = "0.37.0", default-features = false, features = ["bundled", "column_decltype", "functions", "backup", "preupdate_hook"] }
|
||||
rust-embed = { version = "8.4.0", default-features = false, features = ["mime-guess"] }
|
||||
tokio = { version = "^1.38.0", default-features = false, features = ["macros", "net", "rt-multi-thread", "fs", "signal", "time", "sync"] }
|
||||
|
||||
@@ -77,6 +77,12 @@ pub struct GitVersion {
|
||||
pub commits_since: Option<u16>,
|
||||
}
|
||||
|
||||
impl GitVersion {
|
||||
pub fn tag(&self) -> String {
|
||||
return format!("v{}.{}.{}", self.major, self.minor, self.patch);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct VersionInfo {
|
||||
/// Name of the crate as defined in its Cargo.toml.
|
||||
|
||||
@@ -16,17 +16,21 @@ axum = { version = "^0.8.1", features=["multipart"] }
|
||||
chrono = "^0.4.38"
|
||||
clap = { version = "^4.4.11", features=["derive", "env"] }
|
||||
env_logger = { workspace = true }
|
||||
trailbase = { workspace = true }
|
||||
trailbase-build = { workspace = true }
|
||||
itertools = "0.14.0"
|
||||
log = "^0.4.21"
|
||||
mimalloc = { version = "^0.1.41", default-features = false }
|
||||
minijinja = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { version = "^1.0.203", features = ["derive"] }
|
||||
serde_json = "^1.0.117"
|
||||
tokio = { workspace = true }
|
||||
trailbase = { workspace = true }
|
||||
trailbase-build = { workspace = true }
|
||||
url = "2.5.4"
|
||||
utoipa = { version = "5.0.0-beta.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }
|
||||
uuid = { workspace = true }
|
||||
url = "2.5.4"
|
||||
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
|
||||
|
||||
[build-dependencies]
|
||||
trailbase-build = { workspace = true }
|
||||
|
||||
@@ -77,6 +77,11 @@ pub enum SubCommands {
|
||||
},
|
||||
/// Programmatically send emails.
|
||||
Email(EmailArgs),
|
||||
/// Manage WASM components
|
||||
Components {
|
||||
#[command(subcommand)]
|
||||
cmd: Option<ComponentSubCommands>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args, Clone, Debug)]
|
||||
@@ -220,3 +225,56 @@ pub enum UserSubCommands {
|
||||
user: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ComponentReference {
|
||||
Path(std::path::PathBuf),
|
||||
Url(url::Url),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ComponentReference {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(reference: &str) -> Result<Self, Self::Error> {
|
||||
if let Ok(url) = url::Url::parse(reference) {
|
||||
if url.scheme() != "https" {
|
||||
return Err("Only HTTPS supported".into());
|
||||
}
|
||||
|
||||
return Ok(ComponentReference::Url(url));
|
||||
}
|
||||
|
||||
let path = std::path::PathBuf::from(reference);
|
||||
if let Some(ext) = path.extension() {
|
||||
match &*ext.to_string_lossy() {
|
||||
"wasm" | "zip" => {
|
||||
return Ok(ComponentReference::Path(path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if reference
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '/')
|
||||
{
|
||||
return Ok(ComponentReference::Name(reference.into()));
|
||||
}
|
||||
|
||||
return Err("Failed to parse component reference".into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum ComponentSubCommands {
|
||||
/// Add new WASM component.
|
||||
Add {
|
||||
/// File-system path or url for wasm component to install.
|
||||
reference: String,
|
||||
},
|
||||
/// Remove/delete WASM component.
|
||||
Remove { reference: String },
|
||||
/// List first-party components.
|
||||
List,
|
||||
}
|
||||
|
||||
@@ -5,8 +5,14 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use chrono::TimeZone;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use itertools::Itertools;
|
||||
use minijinja::{Environment, context};
|
||||
use serde::Deserialize;
|
||||
use std::rc::Rc;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Read, Seek},
|
||||
rc::Rc,
|
||||
};
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
use trailbase::{
|
||||
DataDir, Server, ServerOptions,
|
||||
@@ -16,7 +22,8 @@ use trailbase::{
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use trailbase_cli::{
|
||||
AdminSubCommands, DefaultCommandLineArgs, OpenApiSubCommands, SubCommands, UserSubCommands,
|
||||
AdminSubCommands, ComponentReference, ComponentSubCommands, DefaultCommandLineArgs,
|
||||
OpenApiSubCommands, SubCommands, UserSubCommands,
|
||||
};
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
@@ -246,12 +253,87 @@ async fn async_main() -> Result<(), BoxError> {
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(SubCommands::Components { cmd }) => {
|
||||
init_logger(false);
|
||||
|
||||
match cmd {
|
||||
Some(ComponentSubCommands::Add { reference }) => {
|
||||
match ComponentReference::try_from(reference.as_str())? {
|
||||
ComponentReference::Name(name) => {
|
||||
let component_def = find_component(&name)?;
|
||||
|
||||
let version = trailbase_build::get_version_info!();
|
||||
let Some(git_version) = version.git_version() else {
|
||||
return Err("missing version".into());
|
||||
};
|
||||
|
||||
let env = Environment::empty();
|
||||
let url_str = env
|
||||
.template_from_named_str("url", &component_def.url_template)?
|
||||
.render(context! {
|
||||
release => git_version.tag(),
|
||||
})?;
|
||||
let url = url::Url::parse(&url_str)?;
|
||||
|
||||
log::info!("Downloading {url}");
|
||||
|
||||
let bytes = reqwest::get(url.clone()).await?.bytes().await?;
|
||||
install_wasm_component(&data_dir, url.path(), std::io::Cursor::new(bytes)).await?;
|
||||
}
|
||||
ComponentReference::Url(url) => {
|
||||
log::info!("Downloading {url}");
|
||||
let bytes = reqwest::get(url.clone()).await?.bytes().await?;
|
||||
install_wasm_component(&data_dir, url.path(), std::io::Cursor::new(bytes)).await?;
|
||||
}
|
||||
ComponentReference::Path(path) => {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
install_wasm_component(&data_dir, &path, std::io::Cursor::new(bytes)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ComponentSubCommands::Remove { reference }) => {
|
||||
match ComponentReference::try_from(reference.as_str())? {
|
||||
ComponentReference::Url(_) => {
|
||||
return Err("URLs not supported for component removal".into());
|
||||
}
|
||||
ComponentReference::Name(name) => {
|
||||
let component_def = find_component(&name)?;
|
||||
let wasm_dir = data_dir.root().join("wasm");
|
||||
|
||||
let filenames: Vec<_> = component_def
|
||||
.wasm_filenames
|
||||
.into_iter()
|
||||
.map(|f| wasm_dir.join(f))
|
||||
.collect();
|
||||
|
||||
for filename in &filenames {
|
||||
std::fs::remove_file(filename)?;
|
||||
}
|
||||
println!("Removed: {filenames:?}");
|
||||
}
|
||||
ComponentReference::Path(path) => {
|
||||
std::fs::remove_file(&path)?;
|
||||
|
||||
println!("Removed: {path:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ComponentSubCommands::List) => {
|
||||
println!("Components:\n\n{}", repo().keys().join("\n"));
|
||||
}
|
||||
_ => {
|
||||
DefaultCommandLineArgs::command()
|
||||
.find_subcommand_mut("component")
|
||||
.map(|cmd| cmd.print_help());
|
||||
}
|
||||
};
|
||||
}
|
||||
None => {
|
||||
let _ = DefaultCommandLineArgs::command().print_help();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn to_user_reference(user: String) -> api::cli::UserReference {
|
||||
@@ -261,6 +343,81 @@ fn to_user_reference(user: String) -> api::cli::UserReference {
|
||||
return api::cli::UserReference::Id(user);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ComponentDefinition {
|
||||
url_template: String,
|
||||
wasm_filenames: Vec<String>,
|
||||
}
|
||||
|
||||
fn repo() -> HashMap<String, ComponentDefinition> {
|
||||
return HashMap::from([
|
||||
("trailbase/auth_ui".to_string(), ComponentDefinition {
|
||||
url_template: "https://github.com/trailbaseio/trailbase/releases/download/{{ release }}/trailbase_{{ release }}_wasm_auth_ui.zip".to_string(),
|
||||
wasm_filenames: vec!["auth_ui_component.wasm".to_string()],
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
fn find_component(name: &str) -> Result<ComponentDefinition, BoxError> {
|
||||
return repo()
|
||||
.get(name)
|
||||
.cloned()
|
||||
.ok_or_else(|| "component not found".into());
|
||||
}
|
||||
|
||||
async fn install_wasm_component(
|
||||
data_dir: &DataDir,
|
||||
path: impl AsRef<std::path::Path>,
|
||||
mut reader: impl Read + Seek,
|
||||
) -> Result<(), BoxError> {
|
||||
let path = path.as_ref();
|
||||
let wasm_dir = data_dir.root().join("wasm");
|
||||
|
||||
match path
|
||||
.extension()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.as_deref()
|
||||
{
|
||||
Some("zip") => {
|
||||
let mut archive = zip::ZipArchive::new(reader)?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
if let Some(path) = file.enclosed_name() {
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("wasm") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(filename) = path.file_name().and_then(|e| e.to_str()) else {
|
||||
return Err(format!("Invalid filename: {:?}", file.name()).into());
|
||||
};
|
||||
let component_file_path = wasm_dir.join(filename);
|
||||
let mut component_file = std::fs::File::create(&component_file_path)?;
|
||||
std::io::copy(&mut file, &mut component_file)?;
|
||||
|
||||
println!("Added: {component_file_path:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("wasm") => {
|
||||
let Some(filename) = path.file_name().and_then(|e| e.to_str()) else {
|
||||
return Err(format!("Invalid filename: {path:?}").into());
|
||||
};
|
||||
|
||||
let component_file_path = wasm_dir.join(filename);
|
||||
let mut component_file = std::fs::File::create(&component_file_path)?;
|
||||
std::io::copy(&mut reader, &mut component_file)?;
|
||||
|
||||
println!("Added: {component_file_path:?}");
|
||||
}
|
||||
_ => {
|
||||
return Err("unexpected format".into());
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn main() -> Result<(), BoxError> {
|
||||
let runtime = Rc::new(
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
mod args;
|
||||
|
||||
pub use args::{
|
||||
AdminSubCommands, DefaultCommandLineArgs, EmailArgs, JsonSchemaModeArg, SubCommands,
|
||||
UserSubCommands,
|
||||
AdminSubCommands, ComponentReference, ComponentSubCommands, DefaultCommandLineArgs, EmailArgs,
|
||||
JsonSchemaModeArg, SubCommands, UserSubCommands,
|
||||
};
|
||||
|
||||
pub use args::OpenApiSubCommands;
|
||||
|
||||
@@ -53,7 +53,7 @@ lazy_static = "1.4.0"
|
||||
lettre = { version = "^0.11.7", default-features = false, features = ["tokio1-rustls-tls", "sendmail-transport", "smtp-transport", "builder"] }
|
||||
log = { version = "^0.4.21", default-features = false }
|
||||
mini-moka = "0.10.3"
|
||||
minijinja = { version = "2.1.2", default-features = false }
|
||||
minijinja = { workspace = true }
|
||||
oauth2 = { version = "5.0.0-alpha.4", default-features = false, features = ["reqwest", "rustls-tls"] }
|
||||
object_store = { version = "0.12.0", default-features = false, features = ["aws", "fs"] }
|
||||
parking_lot = { workspace = true }
|
||||
@@ -63,7 +63,7 @@ prost-reflect = { version = "^0.16.0", default-features = false, features = ["de
|
||||
rand = "^0.9.0"
|
||||
reactivate = { version = "0.4.2", features = ["threadsafe"] }
|
||||
regex = "1.11.0"
|
||||
reqwest = { version = "0.12.8", default-features = false, features = ["rustls-tls", "json"] }
|
||||
reqwest = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
serde = { version = "^1.0.203", features = ["derive"] }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use axum::{Json, extract::State};
|
||||
use serde::Serialize;
|
||||
use trailbase_build::version::GitVersion;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::admin::AdminError as Error;
|
||||
@@ -26,20 +25,9 @@ pub async fn info_handler(State(state): State<AppState>) -> Result<Json<InfoResp
|
||||
|
||||
fn build_info_response(state: &AppState) -> InfoResponse {
|
||||
let version_info = state.version();
|
||||
let git_version = version_info.git_version().map(
|
||||
|GitVersion {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
commits_since,
|
||||
..
|
||||
}| {
|
||||
(
|
||||
format!("v{major}.{minor}.{patch}"),
|
||||
commits_since.unwrap_or(0) as usize,
|
||||
)
|
||||
},
|
||||
);
|
||||
let git_version = version_info
|
||||
.git_version()
|
||||
.map(|v| (v.tag(), v.commits_since.unwrap_or(0) as usize));
|
||||
|
||||
return InfoResponse {
|
||||
compiler: version_info.host_compiler,
|
||||
|
||||
Reference in New Issue
Block a user