refactor: move tools/upend_cli functionality to the cli crate
parent
a724d4c07b
commit
78ba02bdc4
|
@ -430,9 +430,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371"
|
||||
checksum = "6342bd4f5a1205d7f41e94a41a901f5647c938cdfa96036338e8533c9d6c2450"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
|
@ -469,9 +469,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd"
|
||||
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
|
@ -1136,19 +1136,6 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.1"
|
||||
|
@ -1542,12 +1529,6 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.26"
|
||||
|
@ -3654,13 +3635,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
|
||||
checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3942,6 +3923,7 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"rayon",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shadow-rs",
|
||||
|
@ -3958,23 +3940,6 @@ dependencies = [
|
|||
"webpage",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "upend_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"filebuffer",
|
||||
"log",
|
||||
"multibase",
|
||||
"multihash",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
|
|
|
@ -10,7 +10,7 @@ edition = "2018"
|
|||
build = "build.rs"
|
||||
|
||||
[workspace]
|
||||
members = ["cli", "tools/upend_cli"]
|
||||
members = ["cli"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
|
|
|
@ -79,6 +79,7 @@ id3 = { version = "1.0.2", optional = true }
|
|||
kamadak-exif = { version = "0.5.4", optional = true }
|
||||
|
||||
shadow-rs = "0.17"
|
||||
reqwest = { version = "0.11.16", features = ["blocking", "json"] }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = "0.17"
|
||||
|
|
492
cli/src/main.rs
492
cli/src/main.rs
|
@ -1,17 +1,23 @@
|
|||
#[macro_use]
|
||||
extern crate upend;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{middleware, App, HttpServer};
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use filebuffer::FileBuffer;
|
||||
use rand::{thread_rng, Rng};
|
||||
use reqwest::Url;
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::trace;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing_subscriber::filter::{EnvFilter, LevelFilter};
|
||||
use upend::addressing::Address;
|
||||
use upend::database::entry::EntryValue;
|
||||
use upend::util::hash::hash;
|
||||
|
||||
use upend::{
|
||||
common::{build, get_static_dir},
|
||||
|
@ -31,9 +37,72 @@ mod util;
|
|||
mod extractors;
|
||||
mod previews;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name="upend", author, version)]
|
||||
struct Args {
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "upend", author, version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Perform a query against an UpEnd server instance.
|
||||
Query {
|
||||
/// URL of the UpEnd instance to query.
|
||||
#[arg(short, long, default_value="http://localhost:8093")]
|
||||
url: Url,
|
||||
/// The query itself, in L-expression format.
|
||||
query: String,
|
||||
/// Output format
|
||||
#[arg(short, long, default_value = "tsv")]
|
||||
format: OutputFormat,
|
||||
},
|
||||
/// Insert an entry.
|
||||
Insert {
|
||||
/// URL of the UpEnd instance to query.
|
||||
#[arg(short, long)]
|
||||
url: Option<Url>,
|
||||
/// The address of the entity.
|
||||
entity: String,
|
||||
// The attribute.
|
||||
attribute: String,
|
||||
// The value.
|
||||
value: EntryValue,
|
||||
/// Output format
|
||||
#[arg(short, long, default_value = "tsv")]
|
||||
format: OutputFormat,
|
||||
},
|
||||
/// Get the address of a file, attribute or URL.
|
||||
Address {
|
||||
/// Type of input to be addressed
|
||||
_type: AddressType,
|
||||
/// Path to a file, hash...
|
||||
input: String,
|
||||
/// Output format
|
||||
#[arg(short, long, default_value = "tsv")]
|
||||
format: OutputFormat,
|
||||
},
|
||||
/// Start an UpEnd server instance.
|
||||
Serve(ServeArgs),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
enum OutputFormat {
|
||||
Json,
|
||||
Tsv,
|
||||
Raw,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
enum AddressType {
|
||||
/// Hash a file and output its address.
|
||||
File,
|
||||
/// Compute an address from the output of `sha256sum`
|
||||
Sha256sum,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct ServeArgs {
|
||||
/// Directory to serve a vault from.
|
||||
#[arg()]
|
||||
directory: PathBuf,
|
||||
|
@ -92,7 +161,7 @@ struct Args {
|
|||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
let args = Cli::parse();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
|
@ -102,189 +171,272 @@ fn main() -> Result<()> {
|
|||
)
|
||||
.init();
|
||||
|
||||
info!("Starting UpEnd {}...", build::PKG_VERSION);
|
||||
let sys = actix::System::new("upend");
|
||||
match args.command {
|
||||
Commands::Query { url, query, format } => {
|
||||
let api_url = url.join("/api/query")?;
|
||||
|
||||
let job_container = JobContainer::new();
|
||||
debug!("Querying \"{}\"", api_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(api_url).body(query).send()?;
|
||||
|
||||
let vault_path = args.directory;
|
||||
|
||||
let open_result =
|
||||
UpEndDatabase::open(&vault_path, args.reinitialize).expect("failed to open database!");
|
||||
|
||||
let upend = Arc::new(open_result.db);
|
||||
let store = Arc::new(Box::new(
|
||||
FsStore::from_path(args.store_path.unwrap_or_else(|| vault_path.clone())).unwrap(),
|
||||
) as Box<dyn UpStore + Send + Sync>);
|
||||
|
||||
let ui_path = get_static_dir("webui");
|
||||
if ui_path.is_err() {
|
||||
warn!(
|
||||
"Couldn't locate Web UI directory ({:?}), disabling...",
|
||||
ui_path
|
||||
);
|
||||
}
|
||||
|
||||
let ui_enabled = ui_path.is_ok() && !args.no_ui;
|
||||
let browser_enabled = !args.no_desktop && ui_enabled && !args.no_browser;
|
||||
|
||||
let preview_path = upend.path.join("previews");
|
||||
#[cfg(feature = "previews")]
|
||||
let preview_store = Some(Arc::new(crate::previews::PreviewStore::new(
|
||||
preview_path.clone(),
|
||||
store.clone(),
|
||||
)));
|
||||
#[cfg(feature = "previews")]
|
||||
let preview_pool = Some(Arc::new(
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(num_cpus::get() / 2)
|
||||
.build()
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
if args.clean {
|
||||
info!("Cleaning temporary directories...");
|
||||
if preview_path.exists() {
|
||||
std::fs::remove_dir_all(&preview_path).unwrap();
|
||||
debug!("Removed {preview_path:?}");
|
||||
} else {
|
||||
debug!("No preview path exists, continuing...");
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => match format {
|
||||
OutputFormat::Json | OutputFormat::Raw => Ok(println!("{}", response.text()?)),
|
||||
OutputFormat::Tsv => todo!(),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("{}", response.text()?);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Insert {
|
||||
url,
|
||||
entity,
|
||||
attribute,
|
||||
value,
|
||||
format: _,
|
||||
} => {
|
||||
let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
|
||||
let api_url = url.join("/api/obj")?;
|
||||
|
||||
#[cfg(not(feature = "previews"))]
|
||||
let preview_store = None;
|
||||
#[cfg(not(feature = "previews"))]
|
||||
let preview_pool = None;
|
||||
if let EntryValue::Invalid = value {
|
||||
return Err(anyhow!("Invalid entry value."));
|
||||
}
|
||||
|
||||
let mut bind: SocketAddr = args.bind.parse().expect("Incorrect bind format.");
|
||||
let body = json!({
|
||||
"entity": entity,
|
||||
"attribute": attribute,
|
||||
"value": value
|
||||
});
|
||||
|
||||
let secret = args.secret.unwrap_or_else(|| {
|
||||
warn!("No secret supplied, generating one at random.");
|
||||
debug!("Inserting {:?} at \"{}\"", body, api_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.put(api_url).json(&body).send()?;
|
||||
|
||||
thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
});
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => {
|
||||
let data: Vec<String> = response.json()?;
|
||||
Ok(println!("{}", data[0]))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{}", response.text()?);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Address {
|
||||
_type,
|
||||
input,
|
||||
format,
|
||||
} => {
|
||||
let address = match _type {
|
||||
AddressType::File => {
|
||||
let filepath = PathBuf::from(input);
|
||||
debug!("Hashing {:?}...", filepath);
|
||||
let fbuffer = FileBuffer::open(&filepath)?;
|
||||
let digest = hash(&fbuffer);
|
||||
trace!("Finished hashing {:?}...", &filepath);
|
||||
Address::Hash(digest)
|
||||
}
|
||||
AddressType::Sha256sum => {
|
||||
let digest = multibase::Base::Base16Lower.decode(input)?;
|
||||
Address::Hash(upend::util::hash::Hash(digest))
|
||||
}
|
||||
};
|
||||
|
||||
let state = routes::State {
|
||||
upend: upend.clone(),
|
||||
store,
|
||||
job_container: job_container.clone(),
|
||||
preview_store,
|
||||
preview_pool,
|
||||
config: UpEndConfig {
|
||||
vault_name: Some(args.vault_name.unwrap_or_else(|| {
|
||||
vault_path
|
||||
.iter()
|
||||
.last()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})),
|
||||
desktop_enabled: !args.no_desktop,
|
||||
trust_executables: args.trust_executables,
|
||||
key: args.key,
|
||||
secret,
|
||||
},
|
||||
};
|
||||
match format {
|
||||
OutputFormat::Json => Ok(println!("\"{}\"", address)),
|
||||
OutputFormat::Tsv | OutputFormat::Raw => Ok(println!("{}", address)),
|
||||
}
|
||||
}
|
||||
Commands::Serve(args) => {
|
||||
info!("Starting UpEnd {}...", build::PKG_VERSION);
|
||||
let sys = actix::System::new("upend");
|
||||
|
||||
// Start HTTP server
|
||||
let job_container = JobContainer::new();
|
||||
|
||||
let mut cnt = 0;
|
||||
let ui_path = ui_path.ok();
|
||||
let server = loop {
|
||||
let state = state.clone();
|
||||
let ui_path = ui_path.clone();
|
||||
let allowed_origins = args.allow_host.clone();
|
||||
let vault_path = args.directory;
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
let allowed_origins = allowed_origins.clone();
|
||||
let open_result = UpEndDatabase::open(&vault_path, args.reinitialize)
|
||||
.expect("failed to open database!");
|
||||
|
||||
let cors = Cors::default()
|
||||
.allowed_origin("http://localhost")
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
origin.as_bytes().starts_with(b"http://localhost:")
|
||||
})
|
||||
.allowed_origin_fn(move |origin, _req_head| {
|
||||
allowed_origins
|
||||
.iter()
|
||||
.any(|allowed_origin| *allowed_origin == "*" || origin == allowed_origin)
|
||||
})
|
||||
.allow_any_method();
|
||||
let upend = Arc::new(open_result.db);
|
||||
let store = Arc::new(Box::new(
|
||||
FsStore::from_path(args.store_path.unwrap_or_else(|| vault_path.clone())).unwrap(),
|
||||
) as Box<dyn UpStore + Send + Sync>);
|
||||
|
||||
let app = App::new()
|
||||
.wrap(cors)
|
||||
.app_data(actix_web::web::PayloadConfig::new(4_294_967_296))
|
||||
.data(state.clone())
|
||||
.wrap(middleware::Logger::default().exclude("/api/jobs"))
|
||||
.service(routes::login)
|
||||
.service(routes::get_raw)
|
||||
.service(routes::get_thumbnail)
|
||||
.service(routes::get_query)
|
||||
.service(routes::get_object)
|
||||
.service(routes::put_object)
|
||||
.service(routes::put_blob)
|
||||
.service(routes::put_object_attribute)
|
||||
.service(routes::delete_object)
|
||||
.service(routes::get_address)
|
||||
.service(routes::get_all_attributes)
|
||||
.service(routes::api_refresh)
|
||||
.service(routes::list_hier)
|
||||
.service(routes::list_hier_roots)
|
||||
.service(routes::store_info)
|
||||
.service(routes::get_jobs)
|
||||
.service(routes::get_info);
|
||||
let ui_path = get_static_dir("webui");
|
||||
if ui_path.is_err() {
|
||||
warn!(
|
||||
"Couldn't locate Web UI directory ({:?}), disabling...",
|
||||
ui_path
|
||||
);
|
||||
}
|
||||
|
||||
if ui_enabled {
|
||||
if let Some(ui_path) = &ui_path {
|
||||
return app
|
||||
.service(actix_files::Files::new("/", ui_path).index_file("index.html"));
|
||||
let ui_enabled = ui_path.is_ok() && !args.no_ui;
|
||||
let browser_enabled = !args.no_desktop && ui_enabled && !args.no_browser;
|
||||
|
||||
let preview_path = upend.path.join("previews");
|
||||
#[cfg(feature = "previews")]
|
||||
let preview_store = Some(Arc::new(crate::previews::PreviewStore::new(
|
||||
preview_path.clone(),
|
||||
store.clone(),
|
||||
)));
|
||||
#[cfg(feature = "previews")]
|
||||
let preview_pool = Some(Arc::new(
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(num_cpus::get() / 2)
|
||||
.build()
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
if args.clean {
|
||||
info!("Cleaning temporary directories...");
|
||||
if preview_path.exists() {
|
||||
std::fs::remove_dir_all(&preview_path).unwrap();
|
||||
debug!("Removed {preview_path:?}");
|
||||
} else {
|
||||
debug!("No preview path exists, continuing...");
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
});
|
||||
#[cfg(not(feature = "previews"))]
|
||||
let preview_store = None;
|
||||
#[cfg(not(feature = "previews"))]
|
||||
let preview_pool = None;
|
||||
|
||||
let bind_result = server.bind(&bind);
|
||||
if let Ok(server) = bind_result {
|
||||
break server;
|
||||
} else {
|
||||
warn!("Failed to bind at {:?}, trying next port number...", bind);
|
||||
bind.set_port(bind.port() + 1);
|
||||
}
|
||||
let mut bind: SocketAddr = args.bind.parse().expect("Incorrect bind format.");
|
||||
|
||||
if cnt > 32 {
|
||||
panic!("Couldn't start server.")
|
||||
} else {
|
||||
cnt += 1;
|
||||
}
|
||||
};
|
||||
let secret = args.secret.unwrap_or_else(|| {
|
||||
warn!("No secret supplied, generating one at random.");
|
||||
|
||||
info!("Starting server at: {}", &bind);
|
||||
server.run();
|
||||
thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
});
|
||||
|
||||
if !args.no_initial_update {
|
||||
info!("Running initial update...");
|
||||
let initial = open_result.new;
|
||||
block_background::<_, _, anyhow::Error>(move || {
|
||||
let _ = state.store.update(&upend, job_container.clone(), initial);
|
||||
let _ = extractors::extract_all(upend, state.store, job_container);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
let state = routes::State {
|
||||
upend: upend.clone(),
|
||||
store,
|
||||
job_container: job_container.clone(),
|
||||
preview_store,
|
||||
preview_pool,
|
||||
config: UpEndConfig {
|
||||
vault_name: Some(args.vault_name.unwrap_or_else(|| {
|
||||
vault_path
|
||||
.iter()
|
||||
.last()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})),
|
||||
desktop_enabled: !args.no_desktop,
|
||||
trust_executables: args.trust_executables,
|
||||
key: args.key,
|
||||
secret,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
{
|
||||
if browser_enabled && ui_enabled {
|
||||
let ui_result = webbrowser::open(&format!("http://localhost:{}", bind.port()));
|
||||
if ui_result.is_err() {
|
||||
warn!("Could not open UI in browser!");
|
||||
// Start HTTP server
|
||||
|
||||
let mut cnt = 0;
|
||||
let ui_path = ui_path.ok();
|
||||
let server = loop {
|
||||
let state = state.clone();
|
||||
let ui_path = ui_path.clone();
|
||||
let allowed_origins = args.allow_host.clone();
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
let allowed_origins = allowed_origins.clone();
|
||||
|
||||
let cors = Cors::default()
|
||||
.allowed_origin("http://localhost")
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
origin.as_bytes().starts_with(b"http://localhost:")
|
||||
})
|
||||
.allowed_origin_fn(move |origin, _req_head| {
|
||||
allowed_origins.iter().any(|allowed_origin| {
|
||||
*allowed_origin == "*" || origin == allowed_origin
|
||||
})
|
||||
})
|
||||
.allow_any_method();
|
||||
|
||||
let app = App::new()
|
||||
.wrap(cors)
|
||||
.app_data(actix_web::web::PayloadConfig::new(4_294_967_296))
|
||||
.data(state.clone())
|
||||
.wrap(middleware::Logger::default().exclude("/api/jobs"))
|
||||
.service(routes::login)
|
||||
.service(routes::get_raw)
|
||||
.service(routes::get_thumbnail)
|
||||
.service(routes::get_query)
|
||||
.service(routes::get_object)
|
||||
.service(routes::put_object)
|
||||
.service(routes::put_blob)
|
||||
.service(routes::put_object_attribute)
|
||||
.service(routes::delete_object)
|
||||
.service(routes::get_address)
|
||||
.service(routes::get_all_attributes)
|
||||
.service(routes::api_refresh)
|
||||
.service(routes::list_hier)
|
||||
.service(routes::list_hier_roots)
|
||||
.service(routes::store_info)
|
||||
.service(routes::get_jobs)
|
||||
.service(routes::get_info);
|
||||
|
||||
if ui_enabled {
|
||||
if let Some(ui_path) = &ui_path {
|
||||
return app.service(
|
||||
actix_files::Files::new("/", ui_path).index_file("index.html"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
});
|
||||
|
||||
let bind_result = server.bind(&bind);
|
||||
if let Ok(server) = bind_result {
|
||||
break server;
|
||||
} else {
|
||||
warn!("Failed to bind at {:?}, trying next port number...", bind);
|
||||
bind.set_port(bind.port() + 1);
|
||||
}
|
||||
|
||||
if cnt > 32 {
|
||||
panic!("Couldn't start server.")
|
||||
} else {
|
||||
cnt += 1;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Starting server at: {}", &bind);
|
||||
server.run();
|
||||
|
||||
if !args.no_initial_update {
|
||||
info!("Running initial update...");
|
||||
let initial = open_result.new;
|
||||
block_background::<_, _, anyhow::Error>(move || {
|
||||
let _ = state.store.update(&upend, job_container.clone(), initial);
|
||||
let _ = extractors::extract_all(upend, state.store, job_container);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
{
|
||||
if browser_enabled && ui_enabled {
|
||||
let ui_result = webbrowser::open(&format!("http://localhost:{}", bind.port()));
|
||||
if ui_result.is_err() {
|
||||
warn!("Could not open UI in browser!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sys.run()?)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sys.run()?)
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -1,28 +0,0 @@
|
|||
[package]
|
||||
name = "upend_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0.32", features = ["derive", "color"] }
|
||||
reqwest = { version = "0.11.13", features = ["blocking", "json"] }
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
multibase = "0.9"
|
||||
multihash = { version = "*", default-features = false, features = [
|
||||
"alloc",
|
||||
"multihash-impl",
|
||||
"sha2",
|
||||
"identity",
|
||||
] }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
filebuffer = "0.4.0"
|
||||
|
||||
log = "0.4"
|
||||
anyhow = "1.0.68"
|
||||
env_logger = "0.10.0"
|
|
@ -1,114 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use multihash::{Code, Hasher, Multihash, MultihashDigest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Entry {
|
||||
pub entity: String,
|
||||
pub attribute: String,
|
||||
pub value: EntryValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImmutableEntry(pub Entry);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InvariantEntry {
|
||||
pub attribute: String,
|
||||
pub value: EntryValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum EntryValue {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Address(String),
|
||||
Null,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for EntryValue {
|
||||
type Err = std::convert::Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() < 2 {
|
||||
match s.chars().next() {
|
||||
Some('S') => Ok(EntryValue::String("".into())),
|
||||
Some('X') => Ok(EntryValue::Null),
|
||||
_ => Ok(EntryValue::Invalid),
|
||||
}
|
||||
} else {
|
||||
let (type_char, content) = s.split_at(1);
|
||||
match (type_char, content) {
|
||||
("S", content) => Ok(EntryValue::String(String::from(content))),
|
||||
("N", content) => {
|
||||
if let Ok(n) = content.parse::<f64>() {
|
||||
Ok(EntryValue::Number(n))
|
||||
} else {
|
||||
Ok(EntryValue::Invalid)
|
||||
}
|
||||
}
|
||||
("O", content) => {
|
||||
Ok(EntryValue::Address(String::from(content)))
|
||||
}
|
||||
_ => Ok(EntryValue::Invalid),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Hash(pub Vec<u8>);
|
||||
|
||||
impl AsRef<[u8]> for Hash {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash<T: AsRef<[u8]>>(input: T) -> Hash {
|
||||
let mut hasher = multihash::Sha2_256::default();
|
||||
hasher.update(input.as_ref());
|
||||
Hash(Vec::from(hasher.finalize()))
|
||||
}
|
||||
|
||||
fn b58_encode<T: AsRef<[u8]>>(vec: T) -> String {
|
||||
multibase::encode(multibase::Base::Base58Btc, vec.as_ref())
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Address {
|
||||
Hash(Hash),
|
||||
Attribute(String),
|
||||
Url(String),
|
||||
}
|
||||
|
||||
// multihash SHA2-256
|
||||
const SHA2_256: u64 = 0x12;
|
||||
// multihash identity
|
||||
const IDENTITY: u64 = 0x00;
|
||||
|
||||
impl Address {
|
||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||
let hash = match self {
|
||||
Self::Hash(hash) => Multihash::wrap(SHA2_256, &hash.0).map_err(|err| anyhow!(err))?,
|
||||
Self::Attribute(attribute) => {
|
||||
Code::Identity.digest(&[&[b'A'], attribute.as_bytes()].concat())
|
||||
}
|
||||
Self::Url(url) => Code::Identity.digest(&[&[b'X'], url.as_bytes()].concat()),
|
||||
};
|
||||
|
||||
Ok(hash.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
b58_encode(self.encode().map_err(|_| std::fmt::Error)?)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
use anyhow::anyhow;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use filebuffer::FileBuffer;
|
||||
use log::{debug, error, trace};
|
||||
use reqwest::Url;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::common::EntryValue;
|
||||
|
||||
mod common;
|
||||
|
||||
/// Command-line client for UpEnd
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None, arg_required_else_help=true)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Output format
|
||||
#[arg(short, long)]
|
||||
format: Option<OutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
Query {
|
||||
/// URL of the UpEnd instance to query.
|
||||
#[arg(short, long)]
|
||||
url: Option<Url>,
|
||||
/// The query itself, in L-expression format.
|
||||
query: String,
|
||||
},
|
||||
/// Insert an entry.
|
||||
Insert {
|
||||
/// URL of the UpEnd instance to query.
|
||||
#[arg(short, long)]
|
||||
url: Option<Url>,
|
||||
/// The address of the entity.
|
||||
entity: String,
|
||||
// The attribute.
|
||||
attribute: String,
|
||||
// The value.
|
||||
value: common::EntryValue,
|
||||
},
|
||||
/// Get the address of a file, attribute or URL.
|
||||
Address {
|
||||
/// Type of input to be addressed
|
||||
_type: AddressType,
|
||||
/// Path to a file, hash...
|
||||
input: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
enum OutputFormat {
|
||||
Json,
|
||||
Tsv,
|
||||
Raw,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
enum AddressType {
|
||||
/// Hash a file and output its address.
|
||||
File,
|
||||
/// Compute an address from the output of `sha256sum`
|
||||
Sha256sum,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
let args = Args::parse();
|
||||
|
||||
let format = args.format.unwrap_or(OutputFormat::Tsv);
|
||||
|
||||
match args.command {
|
||||
Some(Commands::Query { url, query }) => {
|
||||
let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
|
||||
let api_url = url.join("/api/query")?;
|
||||
|
||||
debug!("Querying \"{}\"", api_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(api_url).body(query).send()?;
|
||||
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => match format {
|
||||
OutputFormat::Json | OutputFormat::Raw => println!("{}", response.text()?),
|
||||
OutputFormat::Tsv => todo!(),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("{}", response.text()?);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Insert {
|
||||
url,
|
||||
entity,
|
||||
attribute,
|
||||
value,
|
||||
}) => {
|
||||
let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
|
||||
let api_url = url.join("/api/obj")?;
|
||||
|
||||
if let EntryValue::Invalid = value {
|
||||
return Err(anyhow!("Invalid entry value."));
|
||||
}
|
||||
|
||||
let body = json!({
|
||||
"entity": entity,
|
||||
"attribute": attribute,
|
||||
"value": value
|
||||
});
|
||||
|
||||
debug!("Inserting {:?} at \"{}\"", body, api_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.put(api_url).json(&body).send()?;
|
||||
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => {
|
||||
let data: Vec<String> = response.json()?;
|
||||
println!("{}", data[0]);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{}", response.text()?);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Address { _type, input }) => {
|
||||
let address = match _type {
|
||||
AddressType::File => {
|
||||
let filepath = PathBuf::from(input);
|
||||
debug!("Hashing {:?}...", filepath);
|
||||
let fbuffer = FileBuffer::open(&filepath)?;
|
||||
let digest = common::hash(&fbuffer);
|
||||
trace!("Finished hashing {:?}...", &filepath);
|
||||
common::Address::Hash(digest)
|
||||
}
|
||||
AddressType::Sha256sum => {
|
||||
let digest = multibase::Base::Base16Lower.decode(input)?;
|
||||
common::Address::Hash(common::Hash(digest))
|
||||
}
|
||||
};
|
||||
|
||||
match format {
|
||||
OutputFormat::Json => println!("\"{}\"", address),
|
||||
OutputFormat::Tsv | OutputFormat::Raw => println!("{}", address),
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue