Upgrade to clap v4 (#2015)
Co-authored-by: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com>
This commit is contained in:
parent
6b02c2f026
commit
cdf97fcb96
18 changed files with 728 additions and 688 deletions
120
Cargo.lock
generated
120
Cargo.lock
generated
|
@ -279,6 +279,55 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is-terminal",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "any_ascii"
|
||||
version = "0.3.2"
|
||||
|
@ -610,7 +659,7 @@ dependencies = [
|
|||
"bitflags",
|
||||
"cexpr 0.6.0",
|
||||
"clang-sys",
|
||||
"clap",
|
||||
"clap 3.2.25",
|
||||
"env_logger 0.9.3",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
|
@ -893,13 +942,52 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
|||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"bitflags",
|
||||
"clap_lex 0.5.0",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
"unicase",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
|
@ -909,6 +997,12 @@ dependencies = [
|
|||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "4.5.0"
|
||||
|
@ -929,6 +1023,12 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "1.9.3"
|
||||
|
@ -1029,7 +1129,7 @@ dependencies = [
|
|||
"atty",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"clap 3.2.25",
|
||||
"criterion-plot",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
|
@ -4083,11 +4183,12 @@ dependencies = [
|
|||
"bung",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap 4.3.0",
|
||||
"fern",
|
||||
"futures 0.3.28",
|
||||
"http",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"jsonwebtoken",
|
||||
"log",
|
||||
"once_cell",
|
||||
|
@ -4107,6 +4208,7 @@ dependencies = [
|
|||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tonic",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
|
@ -4289,6 +4391,16 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
|
|
|
@ -34,11 +34,12 @@ base64 = "0.21.1"
|
|||
bung = "0.1.0"
|
||||
bytes = "1.4.0"
|
||||
chrono = { version = "0.4.24", features = ["serde"] }
|
||||
clap = { version = "3.2.25", features = ["env"] }
|
||||
clap = { version = "4.2.1", features = ["env", "derive", "wrap_help", "unicode"] }
|
||||
fern = { version = "0.6.2", features = ["colored"] }
|
||||
futures = "0.3.28"
|
||||
http = "0.2.9"
|
||||
hyper = "0.14.26"
|
||||
ipnet = "2.7.2"
|
||||
jsonwebtoken = "8.3.0"
|
||||
log = "0.4.17"
|
||||
once_cell = "1.17.1"
|
||||
|
@ -54,6 +55,7 @@ serde_json = "1.0.96"
|
|||
surrealdb = { path = "lib", features = ["protocol-http", "protocol-ws", "rustls"] }
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.1", features = ["macros", "signal"] }
|
||||
tokio-util = { version = "0.7.8", features = ["io"] }
|
||||
tracing = "0.1"
|
||||
tracing-futures = "0.2.5"
|
||||
tracing-opentelemetry = "0.18.0"
|
||||
|
|
32
src/cli/abstraction/mod.rs
Normal file
32
src/cli/abstraction/mod.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use clap::Args;
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub(crate) struct AuthArguments {
|
||||
#[arg(help = "Database authentication username to use when connecting")]
|
||||
#[arg(env = "SURREAL_USER", short = 'u', long = "username", visible_alias = "user")]
|
||||
#[arg(default_value = "root")]
|
||||
pub(crate) username: String,
|
||||
#[arg(help = "Database authentication password to use when connecting")]
|
||||
#[arg(short = 'p', long = "password", visible_alias = "pass")]
|
||||
#[arg(env = "SURREAL_PASS", default_value = "root")]
|
||||
pub(crate) password: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct DatabaseSelectionArguments {
|
||||
#[arg(help = "The namespace selected for the operation")]
|
||||
#[arg(env = "SURREAL_NAMESPACE", long = "namespace", visible_alias = "ns")]
|
||||
pub(crate) namespace: String,
|
||||
#[arg(help = "The database selected for the operation")]
|
||||
#[arg(env = "SURREAL_DATABASE", long = "database", visible_alias = "db")]
|
||||
pub(crate) database: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct DatabaseConnectionArguments {
|
||||
#[arg(help = "Remote database server url to connect to")]
|
||||
#[arg(short = 'e', long = "endpoint", visible_aliases = ["conn"])]
|
||||
#[arg(default_value = "https://cloud.surrealdb.com")]
|
||||
#[arg(value_parser = super::validator::endpoint_valid)]
|
||||
pub(crate) endpoint: String,
|
||||
}
|
|
@ -1,115 +1,134 @@
|
|||
use crate::cli::abstraction::AuthArguments;
|
||||
use crate::cnf::SERVER_AGENT;
|
||||
use crate::err::Error;
|
||||
use reqwest::blocking::Body;
|
||||
use reqwest::blocking::Client;
|
||||
use clap::Args;
|
||||
use futures::TryStreamExt;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::copy;
|
||||
use reqwest::{Body, Client, Response};
|
||||
use std::io::ErrorKind;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::{copy, stdin, stdout, AsyncWrite, AsyncWriteExt};
|
||||
use tokio_util::io::{ReaderStream, StreamReader};
|
||||
|
||||
const TYPE: &str = "application/octet-stream";
|
||||
|
||||
pub fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
#[derive(Args, Debug)]
|
||||
pub struct BackupCommandArguments {
|
||||
#[arg(help = "Path to the remote database or file from which to export")]
|
||||
#[arg(value_parser = super::validator::into_valid)]
|
||||
from: String,
|
||||
#[arg(help = "Path to the remote database or file into which to import")]
|
||||
#[arg(default_value = "-")]
|
||||
#[arg(value_parser = super::validator::into_valid)]
|
||||
into: String,
|
||||
#[command(flatten)]
|
||||
auth: AuthArguments,
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
BackupCommandArguments {
|
||||
from,
|
||||
into,
|
||||
auth: AuthArguments {
|
||||
username: user,
|
||||
password: pass,
|
||||
},
|
||||
}: BackupCommandArguments,
|
||||
) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level("error").init();
|
||||
// Try to parse the specified source file
|
||||
let from = matches.value_of("from").unwrap();
|
||||
// Try to parse the specified output file
|
||||
let into = matches.value_of("into").unwrap();
|
||||
|
||||
// Process the source->destination response
|
||||
if from.ends_with(".db") && into.ends_with(".db") {
|
||||
backup_file_to_file(matches, from, into)
|
||||
} else if from.ends_with(".db") {
|
||||
backup_file_to_http(matches, from, into)
|
||||
} else if into.ends_with(".db") {
|
||||
backup_http_to_file(matches, from, into)
|
||||
} else {
|
||||
backup_http_to_http(matches, from, into)
|
||||
let into_local = into.ends_with(".db");
|
||||
let from_local = from.ends_with(".db");
|
||||
match (from.as_str(), into.as_str()) {
|
||||
// From Stdin -> Into Stdout (are you trying to make an ouroboros?)
|
||||
("-", "-") => Err(Error::OperationUnsupported),
|
||||
// From Stdin -> Into File (possible but meaningless)
|
||||
("-", _) if into_local => Err(Error::OperationUnsupported),
|
||||
// From File -> Into Stdout (possible but meaningless, could be useful for source validation but not for now)
|
||||
(_, "-") if from_local => Err(Error::OperationUnsupported),
|
||||
// From File -> Into File (also possible but meaningless,
|
||||
// but since the original function had this, I would choose to keep it as of now)
|
||||
(from, into) if from_local && into_local => {
|
||||
tokio::fs::copy(from, into).await?;
|
||||
Ok(())
|
||||
}
|
||||
// From File -> Into HTTP
|
||||
(from, into) if from_local => {
|
||||
// Copy the data to the destination
|
||||
let from = OpenOptions::new().read(true).open(from).await?;
|
||||
post_http_sync_body(from, into, &user, &pass).await
|
||||
}
|
||||
// From HTTP -> Into File
|
||||
(from, into) if into_local => {
|
||||
// Try to open the output file
|
||||
let into =
|
||||
OpenOptions::new().write(true).create(true).truncate(true).open(into).await?;
|
||||
backup_http_to_file(from, into, &user, &pass).await
|
||||
}
|
||||
// From HTTP -> Into Stdout
|
||||
(from, "-") => backup_http_to_file(from, stdout(), &user, &pass).await,
|
||||
// From Stdin -> Into File
|
||||
("-", into) => {
|
||||
let from = Body::wrap_stream(ReaderStream::new(stdin()));
|
||||
post_http_sync_body(from, into, &user, &pass).await
|
||||
}
|
||||
// From HTTP -> Into HTTP
|
||||
(from, into) => {
|
||||
// Copy the data to the destination
|
||||
let from = get_http_sync_body(from, &user, &pass).await?;
|
||||
post_http_sync_body(from, into, &user, &pass).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn backup_file_to_file(_: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
|
||||
// Try to open the source file
|
||||
let mut from = OpenOptions::new().read(true).open(from)?;
|
||||
// Try to open the output file
|
||||
let mut into = OpenOptions::new().write(true).create(true).truncate(true).open(into)?;
|
||||
// Copy the data to the destination
|
||||
copy(&mut from, &mut into)?;
|
||||
// Everything OK
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backup_http_to_file(matches: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
|
||||
// Parse the specified username
|
||||
let user = matches.value_of("user").unwrap();
|
||||
// Parse the specified password
|
||||
let pass = matches.value_of("pass").unwrap();
|
||||
// Set the correct source URL
|
||||
let from = format!("{from}/sync");
|
||||
// Try to open the source http
|
||||
let mut from = Client::new()
|
||||
.get(from)
|
||||
.basic_auth(user, Some(pass))
|
||||
.header(USER_AGENT, SERVER_AGENT)
|
||||
.header(CONTENT_TYPE, TYPE)
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
// Try to open the output file
|
||||
let mut into = OpenOptions::new().write(true).create(true).truncate(true).open(into)?;
|
||||
// Copy the data to the destination
|
||||
copy(&mut from, &mut into)?;
|
||||
// Everything OK
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backup_file_to_http(matches: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
|
||||
// Parse the specified username
|
||||
let user = matches.value_of("user").unwrap();
|
||||
// Parse the specified password
|
||||
let pass = matches.value_of("pass").unwrap();
|
||||
// Try to open the source file
|
||||
let from = OpenOptions::new().read(true).open(from)?;
|
||||
// Set the correct output URL
|
||||
let into = format!("{into}/sync");
|
||||
// Copy the data to the destination
|
||||
async fn post_http_sync_body<B: Into<Body>>(
|
||||
from: B,
|
||||
into: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
) -> Result<(), Error> {
|
||||
Client::new()
|
||||
.post(into)
|
||||
.post(format!("{into}/sync"))
|
||||
.basic_auth(user, Some(pass))
|
||||
.header(USER_AGENT, SERVER_AGENT)
|
||||
.header(CONTENT_TYPE, TYPE)
|
||||
.body(from)
|
||||
.send()?
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
// Everything OK
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backup_http_to_http(matches: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
|
||||
// Parse the specified username
|
||||
let user = matches.value_of("user").unwrap();
|
||||
// Parse the specified password
|
||||
let pass = matches.value_of("pass").unwrap();
|
||||
// Set the correct source URL
|
||||
let from = format!("{from}/sync");
|
||||
// Set the correct output URL
|
||||
let into = format!("{into}/sync");
|
||||
// Try to open the source file
|
||||
let from = Client::new()
|
||||
.get(from)
|
||||
async fn get_http_sync_body(from: &str, user: &str, pass: &str) -> Result<Response, Error> {
|
||||
Ok(Client::new()
|
||||
.get(format!("{from}/sync"))
|
||||
.basic_auth(user, Some(pass))
|
||||
.header(USER_AGENT, SERVER_AGENT)
|
||||
.header(CONTENT_TYPE, TYPE)
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?)
|
||||
}
|
||||
|
||||
async fn backup_http_to_file<W: AsyncWrite + Unpin>(
|
||||
from: &str,
|
||||
mut into: W,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
) -> Result<(), Error> {
|
||||
let mut from = StreamReader::new(
|
||||
get_http_sync_body(from, user, pass)
|
||||
.await?
|
||||
.bytes_stream()
|
||||
.map_err(|x| std::io::Error::new(ErrorKind::Other, x)),
|
||||
);
|
||||
|
||||
// Copy the data to the destination
|
||||
Client::new()
|
||||
.post(into)
|
||||
.basic_auth(user, Some(pass))
|
||||
.header(USER_AGENT, SERVER_AGENT)
|
||||
.header(CONTENT_TYPE, TYPE)
|
||||
.body(Body::new(from))
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
copy(&mut from, &mut into).await?;
|
||||
into.flush().await?;
|
||||
// Everything OK
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use once_cell::sync::OnceCell;
|
||||
use std::net::SocketAddr;
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
pub static CF: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
|
@ -10,32 +10,6 @@ pub struct Config {
|
|||
pub path: String,
|
||||
pub user: String,
|
||||
pub pass: Option<String>,
|
||||
pub crt: Option<String>,
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
pub fn init(matches: &clap::ArgMatches) {
|
||||
// Parse the server binding address
|
||||
let bind = matches.value_of("bind").unwrap().parse::<SocketAddr>().unwrap();
|
||||
// Parse the database endpoint path
|
||||
let path = matches.value_of("path").unwrap().to_owned();
|
||||
// Parse the root username for authentication
|
||||
let user = matches.value_of("user").unwrap().to_owned();
|
||||
// Parse the root password for authentication
|
||||
let pass = matches.value_of("pass").map(|v| v.to_owned());
|
||||
// Parse any TLS server security options
|
||||
let crt = matches.value_of("web-crt").map(|v| v.to_owned());
|
||||
let key = matches.value_of("web-key").map(|v| v.to_owned());
|
||||
// Check if database strict mode is enabled
|
||||
let strict = matches.is_present("strict");
|
||||
// Store the new config object
|
||||
let _ = CF.set(Config {
|
||||
strict,
|
||||
bind,
|
||||
path,
|
||||
user,
|
||||
pass,
|
||||
crt,
|
||||
key,
|
||||
});
|
||||
pub crt: Option<PathBuf>,
|
||||
pub key: Option<PathBuf>,
|
||||
}
|
||||
|
|
|
@ -1,28 +1,54 @@
|
|||
use crate::cli::abstraction::{
|
||||
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionArguments,
|
||||
};
|
||||
use crate::cli::LOG;
|
||||
use crate::err::Error;
|
||||
use clap::Args;
|
||||
use surrealdb::engine::any::connect;
|
||||
use surrealdb::error::Api as ApiError;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::Error as SurrealError;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ExportCommandArguments {
|
||||
#[arg(help = "Path to the sql file to export. Use dash - to write into stdout.")]
|
||||
#[arg(default_value = "-")]
|
||||
#[arg(index = 1)]
|
||||
file: String,
|
||||
|
||||
#[command(flatten)]
|
||||
conn: DatabaseConnectionArguments,
|
||||
#[command(flatten)]
|
||||
auth: AuthArguments,
|
||||
#[command(flatten)]
|
||||
sel: DatabaseSelectionArguments,
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
ExportCommandArguments {
|
||||
file,
|
||||
conn: DatabaseConnectionArguments {
|
||||
endpoint,
|
||||
},
|
||||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
namespace: ns,
|
||||
database: db,
|
||||
},
|
||||
}: ExportCommandArguments,
|
||||
) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level("error").init();
|
||||
// Try to parse the file argument
|
||||
let file = matches.value_of("file").unwrap();
|
||||
// Parse all other cli arguments
|
||||
let username = matches.value_of("user").unwrap();
|
||||
let password = matches.value_of("pass").unwrap();
|
||||
let endpoint = matches.value_of("conn").unwrap();
|
||||
let ns = matches.value_of("ns").unwrap();
|
||||
let db = matches.value_of("db").unwrap();
|
||||
|
||||
// Connect to the database engine
|
||||
let client = connect(endpoint).await?;
|
||||
// Sign in to the server if the specified database engine supports it
|
||||
let root = Root {
|
||||
username,
|
||||
password,
|
||||
username: &username,
|
||||
password: &password,
|
||||
};
|
||||
if let Err(error) = client.signin(root).await {
|
||||
match error {
|
||||
|
|
|
@ -1,28 +1,52 @@
|
|||
use crate::cli::abstraction::{
|
||||
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionArguments,
|
||||
};
|
||||
use crate::cli::LOG;
|
||||
use crate::err::Error;
|
||||
use clap::Args;
|
||||
use surrealdb::engine::any::connect;
|
||||
use surrealdb::error::Api as ApiError;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::Error as SurrealError;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ImportCommandArguments {
|
||||
#[arg(help = "Path to the sql file to import")]
|
||||
#[arg(index = 1)]
|
||||
file: String,
|
||||
#[command(flatten)]
|
||||
conn: DatabaseConnectionArguments,
|
||||
#[command(flatten)]
|
||||
auth: AuthArguments,
|
||||
#[command(flatten)]
|
||||
sel: DatabaseSelectionArguments,
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
ImportCommandArguments {
|
||||
file,
|
||||
conn: DatabaseConnectionArguments {
|
||||
endpoint,
|
||||
},
|
||||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
namespace: ns,
|
||||
database: db,
|
||||
},
|
||||
}: ImportCommandArguments,
|
||||
) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level("error").init();
|
||||
// Try to parse the file argument
|
||||
let file = matches.value_of("file").unwrap();
|
||||
// Parse all other cli arguments
|
||||
let username = matches.value_of("user").unwrap();
|
||||
let password = matches.value_of("pass").unwrap();
|
||||
let endpoint = matches.value_of("conn").unwrap();
|
||||
let ns = matches.value_of("ns").unwrap();
|
||||
let db = matches.value_of("db").unwrap();
|
||||
|
||||
// Connect to the database engine
|
||||
let client = connect(endpoint).await?;
|
||||
// Sign in to the server if the specified database engine supports it
|
||||
let root = Root {
|
||||
username,
|
||||
password,
|
||||
username: &username,
|
||||
password: &password,
|
||||
};
|
||||
if let Err(error) = client.signin(root).await {
|
||||
match error {
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
use crate::cli::abstraction::DatabaseConnectionArguments;
|
||||
use crate::err::Error;
|
||||
use clap::Args;
|
||||
use surrealdb::engine::any::connect;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
#[derive(Args, Debug)]
|
||||
pub struct IsReadyCommandArguments {
|
||||
#[command(flatten)]
|
||||
conn: DatabaseConnectionArguments,
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
IsReadyCommandArguments {
|
||||
conn: DatabaseConnectionArguments {
|
||||
endpoint,
|
||||
},
|
||||
}: IsReadyCommandArguments,
|
||||
) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level("error").init();
|
||||
// Parse all other cli arguments
|
||||
let endpoint = matches.value_of("conn").unwrap();
|
||||
// Connect to the database engine
|
||||
connect(endpoint).await?;
|
||||
println!("OK");
|
||||
|
|
544
src/cli/mod.rs
544
src/cli/mod.rs
|
@ -1,3 +1,4 @@
|
|||
pub(crate) mod abstraction;
|
||||
mod backup;
|
||||
mod config;
|
||||
mod export;
|
||||
|
@ -5,17 +6,20 @@ mod import;
|
|||
mod isready;
|
||||
mod sql;
|
||||
mod start;
|
||||
pub(crate) mod validator;
|
||||
mod version;
|
||||
|
||||
pub use config::CF;
|
||||
|
||||
use crate::cnf::LOGO;
|
||||
use clap::{Arg, Command};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use backup::BackupCommandArguments;
|
||||
use clap::{Parser, Subcommand};
|
||||
use export::ExportCommandArguments;
|
||||
use import::ImportCommandArguments;
|
||||
use isready::IsReadyCommandArguments;
|
||||
use sql::SqlCommandArguments;
|
||||
use start::StartCommandArguments;
|
||||
use std::process::ExitCode;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
pub const LOG: &str = "surrealdb::cli";
|
||||
|
||||
|
@ -32,503 +36,51 @@ We would love it if you could star the repository (https://github.com/surrealdb/
|
|||
----------
|
||||
";
|
||||
|
||||
fn split_endpoint(v: &str) -> (&str, &str) {
|
||||
match v {
|
||||
"memory" => ("mem", ""),
|
||||
v => match v.split_once("://") {
|
||||
Some(parts) => parts,
|
||||
None => v.split_once(':').unwrap_or_default(),
|
||||
},
|
||||
}
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "SurrealDB command-line interface and server", bin_name = "surreal")]
|
||||
#[command(about = INFO, before_help = LOGO)]
|
||||
#[command(disable_version_flag = true, arg_required_else_help = true)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
fn file_valid(v: &str) -> Result<(), String> {
|
||||
match v {
|
||||
v if !v.is_empty() => Ok(()),
|
||||
_ => Err(String::from("Provide a valid path to a SQL file")),
|
||||
}
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
#[command(about = "Start the database server")]
|
||||
Start(StartCommandArguments),
|
||||
#[command(about = "Backup data to or from an existing database")]
|
||||
Backup(BackupCommandArguments),
|
||||
#[command(about = "Import a SurrealQL script into an existing database")]
|
||||
Import(ImportCommandArguments),
|
||||
#[command(about = "Export an existing database as a SurrealQL script")]
|
||||
Export(ExportCommandArguments),
|
||||
#[command(about = "Output the command-line tool version information")]
|
||||
Version,
|
||||
#[command(about = "Start an SQL REPL in your terminal with pipe support")]
|
||||
Sql(SqlCommandArguments),
|
||||
#[command(
|
||||
about = "Check if the SurrealDB server is ready to accept connections",
|
||||
visible_alias = "isready"
|
||||
)]
|
||||
IsReady(IsReadyCommandArguments),
|
||||
}
|
||||
|
||||
fn file_exists(file: &str) -> Result<(), String> {
|
||||
let path = Path::new(file);
|
||||
if !*path.try_exists().as_ref().map_err(ToString::to_string)? {
|
||||
return Err(String::from("Ensure the file exists"));
|
||||
}
|
||||
if !path.is_file() {
|
||||
return Err(String::from("Ensure the path is a file"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bind_valid(v: &str) -> Result<(), String> {
|
||||
match v.parse::<SocketAddr>() {
|
||||
Ok(_) => Ok(()),
|
||||
_ => Err(String::from("Provide a valid network bind parameter")),
|
||||
}
|
||||
}
|
||||
|
||||
fn path_valid(v: &str) -> Result<(), String> {
|
||||
match v {
|
||||
"memory" => Ok(()),
|
||||
v if v.starts_with("file:") => Ok(()),
|
||||
v if v.starts_with("rocksdb:") => Ok(()),
|
||||
v if v.starts_with("tikv:") => Ok(()),
|
||||
v if v.starts_with("fdb:") => Ok(()),
|
||||
_ => Err(String::from("Provide a valid database path parameter")),
|
||||
}
|
||||
}
|
||||
|
||||
fn conn_valid(v: &str) -> Result<(), String> {
|
||||
let scheme = split_endpoint(v).0;
|
||||
match scheme {
|
||||
"http" | "https" | "ws" | "wss" | "fdb" | "mem" | "rocksdb" | "file" | "tikv" => Ok(()),
|
||||
_ => Err(String::from("Provide a valid database connection string")),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_valid(v: &str) -> Result<(), String> {
|
||||
match v {
|
||||
v if v.ends_with(".db") => Ok(()),
|
||||
v if v.starts_with("http://") => Ok(()),
|
||||
v if v.starts_with("https://") => Ok(()),
|
||||
_ => Err(String::from("Provide a valid database connection string, or the path to a file")),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_valid(v: &str) -> Result<(), String> {
|
||||
match v {
|
||||
v if v.ends_with(".db") => Ok(()),
|
||||
v if v.starts_with("http://") => Ok(()),
|
||||
v if v.starts_with("https://") => Ok(()),
|
||||
_ => Err(String::from("Provide a valid database connection string, or the path to a file")),
|
||||
}
|
||||
}
|
||||
|
||||
fn key_valid(v: &str) -> Result<(), String> {
|
||||
match v.len() {
|
||||
16 => Ok(()),
|
||||
24 => Ok(()),
|
||||
32 => Ok(()),
|
||||
_ => Err(String::from("Ensure your database encryption key is 16, 24, or 32 bits long")),
|
||||
}
|
||||
}
|
||||
|
||||
fn log_valid(v: &str) -> Result<String, String> {
|
||||
match v {
|
||||
// Don't show any logs at all
|
||||
"none" => Ok("none".to_string()),
|
||||
// Check if we should show all log levels
|
||||
"full" => Ok(Level::TRACE.to_string()),
|
||||
// Otherwise, let's only show errors
|
||||
"error" => Ok(Level::ERROR.to_string()),
|
||||
// Specify the log level for each code area
|
||||
"warn" | "info" | "debug" | "trace" => {
|
||||
Ok(format!("error,surreal={v},surrealdb={v},surrealdb::txn=error"))
|
||||
}
|
||||
// Let's try to parse the custom log level
|
||||
_ => match EnvFilter::builder().parse(v) {
|
||||
// The custom log level parsed successfully
|
||||
Ok(_) => Ok(v.to_owned()),
|
||||
// There was an error parsing the custom log level
|
||||
Err(_) => Err(String::from("Provide a valid log filter configuration string")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> ExitCode {
|
||||
let setup = Command::new("SurrealDB command-line interface and server")
|
||||
.about(INFO)
|
||||
.before_help(LOGO)
|
||||
.disable_version_flag(true)
|
||||
.arg_required_else_help(true);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("start")
|
||||
.display_order(1)
|
||||
.about("Start the database server")
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.index(1)
|
||||
.env("SURREAL_PATH")
|
||||
.required(false)
|
||||
.validator(path_valid)
|
||||
.default_value("memory")
|
||||
.help("Database path used for storing data"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("user")
|
||||
.short('u')
|
||||
.env("SURREAL_USER")
|
||||
.long("user")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("The master username for the database"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pass")
|
||||
.short('p')
|
||||
.env("SURREAL_PASS")
|
||||
.long("pass")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The master password for the database"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("addr")
|
||||
.env("SURREAL_ADDR")
|
||||
.long("addr")
|
||||
.number_of_values(1)
|
||||
.forbid_empty_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.default_value("127.0.0.1/32")
|
||||
.help("The allowed networks for master authentication"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("bind")
|
||||
.short('b')
|
||||
.env("SURREAL_BIND")
|
||||
.long("bind")
|
||||
.validator(bind_valid)
|
||||
.forbid_empty_values(true)
|
||||
.default_value("0.0.0.0:8000")
|
||||
.help("The hostname or ip address to listen for connections on"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("key")
|
||||
.short('k')
|
||||
.env("SURREAL_KEY")
|
||||
.long("key")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.validator(key_valid)
|
||||
.help("Encryption key to use for on-disk encryption"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("kvs-ca")
|
||||
.env("SURREAL_KVS_CA")
|
||||
.long("kvs-ca")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.validator(file_exists)
|
||||
.help("Path to the CA file used when connecting to the remote KV store"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("kvs-crt")
|
||||
.env("SURREAL_KVS_CRT")
|
||||
.long("kvs-crt")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.validator(file_exists)
|
||||
.help(
|
||||
"Path to the certificate file used when connecting to the remote KV store",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("kvs-key")
|
||||
.env("SURREAL_KVS_KEY")
|
||||
.long("kvs-key")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.validator(file_exists)
|
||||
.help(
|
||||
"Path to the private key file used when connecting to the remote KV store",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("web-crt")
|
||||
.env("SURREAL_WEB_CRT")
|
||||
.long("web-crt")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.validator(file_exists)
|
||||
.help("Path to the certificate file for encrypted client connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("web-key")
|
||||
.env("SURREAL_WEB_KEY")
|
||||
.long("web-key")
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.validator(file_exists)
|
||||
.help("Path to the private key file for encrypted client connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("strict")
|
||||
.short('s')
|
||||
.env("SURREAL_STRICT")
|
||||
.long("strict")
|
||||
.required(false)
|
||||
.takes_value(false)
|
||||
.help("Whether strict mode is enabled on this database instance"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("log")
|
||||
.short('l')
|
||||
.env("SURREAL_LOG")
|
||||
.long("log")
|
||||
.takes_value(true)
|
||||
.default_value("info")
|
||||
.forbid_empty_values(true)
|
||||
.value_parser(log_valid)
|
||||
.help("The logging level for the database server. One of error, warn, info, debug, trace, full."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-banner")
|
||||
.env("SURREAL_NO_BANNER")
|
||||
.long("no-banner")
|
||||
.required(false)
|
||||
.takes_value(false)
|
||||
.help("Whether to hide the startup banner"),
|
||||
),
|
||||
);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("backup")
|
||||
.display_order(2)
|
||||
.about("Backup data to or from an existing database")
|
||||
.arg(
|
||||
Arg::new("from")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.validator(from_valid)
|
||||
.help("Path to the remote database or file from which to export"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("into")
|
||||
.index(2)
|
||||
.required(true)
|
||||
.validator(into_valid)
|
||||
.help("Path to the remote database or file into which to import"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("user")
|
||||
.short('u')
|
||||
.long("user")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication username to use when connecting"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pass")
|
||||
.short('p')
|
||||
.long("pass")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication password to use when connecting"),
|
||||
),
|
||||
);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("import")
|
||||
.display_order(3)
|
||||
.about("Import a SurrealQL script into an existing database")
|
||||
.arg(
|
||||
Arg::new("file")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.validator(file_valid)
|
||||
.help("Path to the sql file to import"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ns")
|
||||
.long("ns")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The namespace to import the data into"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("db")
|
||||
.long("db")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The database to import the data into"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("conn")
|
||||
.short('c')
|
||||
.long("conn")
|
||||
.alias("host")
|
||||
.forbid_empty_values(true)
|
||||
.validator(conn_valid)
|
||||
.default_value("https://cloud.surrealdb.com")
|
||||
.help("Remote database server url to connect to"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("user")
|
||||
.short('u')
|
||||
.long("user")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication username to use when connecting"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pass")
|
||||
.short('p')
|
||||
.long("pass")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication password to use when connecting"),
|
||||
),
|
||||
);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("export")
|
||||
.display_order(4)
|
||||
.about("Export an existing database as a SurrealQL script")
|
||||
.arg(
|
||||
Arg::new("file")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.validator(file_valid)
|
||||
.help("Path to the sql file to export. Use dash - to write into stdout."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ns")
|
||||
.long("ns")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The namespace to export the data from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("db")
|
||||
.long("db")
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The database to export the data from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("conn")
|
||||
.short('c')
|
||||
.long("conn")
|
||||
.alias("host")
|
||||
.forbid_empty_values(true)
|
||||
.validator(conn_valid)
|
||||
.default_value("https://cloud.surrealdb.com")
|
||||
.help("Remote database server url to connect to"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("user")
|
||||
.short('u')
|
||||
.long("user")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication username to use when connecting"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pass")
|
||||
.short('p')
|
||||
.long("pass")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication password to use when connecting"),
|
||||
),
|
||||
);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("version")
|
||||
.display_order(5)
|
||||
.about("Output the command-line tool version information"),
|
||||
);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("sql")
|
||||
.display_order(6)
|
||||
.about("Start an SQL REPL in your terminal with pipe support")
|
||||
.arg(
|
||||
Arg::new("ns")
|
||||
.long("ns")
|
||||
.required(false)
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The namespace to export the data from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("db")
|
||||
.long("db")
|
||||
.required(false)
|
||||
.takes_value(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("The database to export the data from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("conn")
|
||||
.short('c')
|
||||
.long("conn")
|
||||
.alias("host")
|
||||
.forbid_empty_values(true)
|
||||
.validator(conn_valid)
|
||||
.default_value("wss://cloud.surrealdb.com")
|
||||
.help("Remote database server url to connect to"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("user")
|
||||
.short('u')
|
||||
.long("user")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication username to use when connecting"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("multi")
|
||||
.long("multi")
|
||||
.required(false)
|
||||
.takes_value(false)
|
||||
.help("Whether omitting semicolon causes a newline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pass")
|
||||
.short('p')
|
||||
.long("pass")
|
||||
.forbid_empty_values(true)
|
||||
.default_value("root")
|
||||
.help("Database authentication password to use when connecting"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pretty")
|
||||
.long("pretty")
|
||||
.required(false)
|
||||
.takes_value(false)
|
||||
.help("Whether database responses should be pretty printed"),
|
||||
),
|
||||
);
|
||||
|
||||
let setup = setup.subcommand(
|
||||
Command::new("isready")
|
||||
.display_order(7)
|
||||
.about("Check if the SurrealDB server is ready to accept connections")
|
||||
.arg(
|
||||
Arg::new("conn")
|
||||
.short('c')
|
||||
.long("conn")
|
||||
.alias("host")
|
||||
.forbid_empty_values(true)
|
||||
.validator(conn_valid)
|
||||
.default_value("http://localhost:8000")
|
||||
.help("Remote database server url to connect to"),
|
||||
),
|
||||
);
|
||||
|
||||
let matches = setup.get_matches();
|
||||
|
||||
let output = match matches.subcommand() {
|
||||
Some(("sql", m)) => sql::init(m),
|
||||
Some(("start", m)) => start::init(m),
|
||||
Some(("backup", m)) => backup::init(m),
|
||||
Some(("import", m)) => import::init(m),
|
||||
Some(("export", m)) => export::init(m),
|
||||
Some(("version", m)) => version::init(m),
|
||||
Some(("isready", m)) => isready::init(m),
|
||||
_ => Ok(()),
|
||||
pub async fn init() -> ExitCode {
|
||||
let args = Cli::parse();
|
||||
let output = match args.command {
|
||||
Commands::Start(args) => start::init(args).await,
|
||||
Commands::Backup(args) => backup::init(args).await,
|
||||
Commands::Import(args) => import::init(args).await,
|
||||
Commands::Export(args) => export::init(args).await,
|
||||
Commands::Version => version::init(),
|
||||
Commands::Sql(args) => sql::init(args).await,
|
||||
Commands::IsReady(args) => isready::init(args).await,
|
||||
};
|
||||
|
||||
if let Err(e) = output {
|
||||
error!(target: LOG, "{}", e);
|
||||
return ExitCode::FAILURE;
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use crate::cli::abstraction::{
|
||||
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionArguments,
|
||||
};
|
||||
use crate::err::Error;
|
||||
use clap::Args;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
|
||||
use rustyline::{Completer, Editor, Helper, Highlighter, Hinter};
|
||||
|
@ -8,24 +12,49 @@ use surrealdb::opt::auth::Root;
|
|||
use surrealdb::sql::{self, Statement, Value};
|
||||
use surrealdb::{Error as SurrealError, Response};
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
#[derive(Args, Debug)]
|
||||
pub struct SqlCommandArguments {
|
||||
#[command(flatten)]
|
||||
conn: DatabaseConnectionArguments,
|
||||
#[command(flatten)]
|
||||
auth: AuthArguments,
|
||||
#[command(flatten)]
|
||||
sel: DatabaseSelectionArguments,
|
||||
/// Whether database responses should be pretty printed
|
||||
#[arg(long)]
|
||||
pretty: bool,
|
||||
/// Whether omitting semicolon causes a newline
|
||||
#[arg(long)]
|
||||
multi: bool,
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
SqlCommandArguments {
|
||||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
conn: DatabaseConnectionArguments {
|
||||
endpoint,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
namespace,
|
||||
database,
|
||||
},
|
||||
pretty,
|
||||
multi,
|
||||
..
|
||||
}: SqlCommandArguments,
|
||||
) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level("warn").init();
|
||||
// Parse all other cli arguments
|
||||
let username = matches.value_of("user").unwrap();
|
||||
let password = matches.value_of("pass").unwrap();
|
||||
let endpoint = matches.value_of("conn").unwrap();
|
||||
let mut ns = matches.value_of("ns").map(str::to_string);
|
||||
let mut db = matches.value_of("db").map(str::to_string);
|
||||
// If we should pretty-print responses
|
||||
let pretty = matches.is_present("pretty");
|
||||
|
||||
// Connect to the database engine
|
||||
let client = connect(endpoint).await?;
|
||||
// Sign in to the server if the specified database engine supports it
|
||||
let root = Root {
|
||||
username,
|
||||
password,
|
||||
username: &username,
|
||||
password: &password,
|
||||
};
|
||||
if let Err(error) = client.signin(root).await {
|
||||
match error {
|
||||
|
@ -40,10 +69,13 @@ pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
|||
let mut rl = Editor::new().unwrap();
|
||||
// Set custom input validation
|
||||
rl.set_helper(Some(InputValidator {
|
||||
multi: matches.is_present("multi"),
|
||||
multi,
|
||||
}));
|
||||
// Load the command-line history
|
||||
let _ = rl.load_history("history.txt");
|
||||
// Keep track of current namespace/database.
|
||||
let mut ns = Some(namespace);
|
||||
let mut db = Some(database);
|
||||
// Configure the prompt
|
||||
let mut prompt = "> ".to_owned();
|
||||
// Loop over each command-line input
|
||||
|
|
123
src/cli/start.rs
123
src/cli/start.rs
|
@ -1,26 +1,119 @@
|
|||
use super::config;
|
||||
use super::config::Config;
|
||||
use crate::cli::validator::parser::env_filter::CustomEnvFilter;
|
||||
use crate::cli::validator::parser::env_filter::CustomEnvFilterParser;
|
||||
use crate::cnf::LOGO;
|
||||
use crate::dbs;
|
||||
use crate::env;
|
||||
use crate::err::Error;
|
||||
use crate::iam;
|
||||
use crate::net;
|
||||
use futures::Future;
|
||||
use clap::Args;
|
||||
use ipnet::IpNet;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
with_enough_stack(init_impl(matches))
|
||||
#[derive(Args, Debug)]
|
||||
pub struct StartCommandArguments {
|
||||
#[arg(help = "Database path used for storing data")]
|
||||
#[arg(env = "SURREAL_PATH", index = 1)]
|
||||
#[arg(default_value = "memory")]
|
||||
#[arg(value_parser = super::validator::path_valid)]
|
||||
path: String,
|
||||
#[arg(help = "The master username for the database")]
|
||||
#[arg(env = "SURREAL_USER", short = 'u', long = "username", visible_alias = "user")]
|
||||
#[arg(default_value = "root")]
|
||||
username: String,
|
||||
#[arg(help = "The master password for the database")]
|
||||
#[arg(env = "SURREAL_PASS", short = 'p', long = "password", visible_alias = "pass")]
|
||||
password: Option<String>,
|
||||
#[arg(help = "The allowed networks for master authentication")]
|
||||
#[arg(env = "SURREAL_ADDR", long = "addr")]
|
||||
#[arg(default_value = "127.0.0.1/32")]
|
||||
allowed_networks: Vec<IpNet>,
|
||||
#[arg(help = "The hostname or ip address to listen for connections on")]
|
||||
#[arg(env = "SURREAL_BIND", short = 'b', long = "bind")]
|
||||
#[arg(default_value = "0.0.0.0:8000")]
|
||||
listen_addresses: Vec<SocketAddr>,
|
||||
#[arg(help = "Encryption key to use for on-disk encryption")]
|
||||
#[arg(env = "SURREAL_KEY", short = 'k', long = "key")]
|
||||
#[arg(value_parser = super::validator::key_valid)]
|
||||
key: Option<String>,
|
||||
#[command(flatten)]
|
||||
kvs: Option<StartCommandRemoteTlsOptions>,
|
||||
#[command(flatten)]
|
||||
web: Option<StartCommandWebTlsOptions>,
|
||||
#[arg(help = "Whether strict mode is enabled on this database instance")]
|
||||
#[arg(env = "SURREAL_STRICT", short = 's', long = "strict")]
|
||||
#[arg(default_value_t = false)]
|
||||
strict: bool,
|
||||
#[arg(help = "The logging level for the database server")]
|
||||
#[arg(env = "SURREAL_LOG", short = 'l', long = "log")]
|
||||
#[arg(default_value = "info")]
|
||||
#[arg(value_parser = CustomEnvFilterParser::new())]
|
||||
log: CustomEnvFilter,
|
||||
#[arg(help = "Whether to hide the startup banner")]
|
||||
#[arg(env = "SURREAL_NO_BANNER", long)]
|
||||
#[arg(default_value_t = false)]
|
||||
no_banner: bool,
|
||||
}
|
||||
|
||||
async fn init_impl(matches: &clap::ArgMatches) -> Result<(), Error> {
|
||||
#[derive(Args, Debug)]
|
||||
#[group(requires_all = ["kvs_ca", "kvs_crt", "kvs_key"], multiple = true)]
|
||||
struct StartCommandRemoteTlsOptions {
|
||||
#[arg(help = "Path to the CA file used when connecting to the remote KV store")]
|
||||
#[arg(env = "SURREAL_KVS_CA", long = "kvs-ca", value_parser = super::validator::file_exists)]
|
||||
kvs_ca: Option<PathBuf>,
|
||||
#[arg(help = "Path to the certificate file used when connecting to the remote KV store")]
|
||||
#[arg(env = "SURREAL_KVS_CRT", long = "kvs-crt", value_parser = super::validator::file_exists)]
|
||||
kvs_crt: Option<PathBuf>,
|
||||
#[arg(help = "Path to the private key file used when connecting to the remote KV store")]
|
||||
#[arg(env = "SURREAL_KVS_KEY", long = "kvs-key", value_parser = super::validator::file_exists)]
|
||||
kvs_key: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
#[group(requires_all = ["web_crt", "web_key"], multiple = true)]
|
||||
struct StartCommandWebTlsOptions {
|
||||
#[arg(help = "Path to the certificate file for encrypted client connections")]
|
||||
#[arg(env = "SURREAL_WEB_CRT", long = "web-crt", value_parser = super::validator::file_exists)]
|
||||
web_crt: Option<PathBuf>,
|
||||
#[arg(help = "Path to the private key file for encrypted client connections")]
|
||||
#[arg(env = "SURREAL_WEB_KEY", long = "web-key", value_parser = super::validator::file_exists)]
|
||||
web_key: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
StartCommandArguments {
|
||||
path,
|
||||
username: user,
|
||||
password: pass,
|
||||
listen_addresses,
|
||||
web,
|
||||
strict,
|
||||
log: CustomEnvFilter(log),
|
||||
no_banner,
|
||||
..
|
||||
}: StartCommandArguments,
|
||||
) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level(matches.get_one::<String>("log").unwrap()).init();
|
||||
crate::o11y::builder().with_filter(log).init();
|
||||
|
||||
// Check if a banner should be outputted
|
||||
if !matches.is_present("no-banner") {
|
||||
if !no_banner {
|
||||
// Output SurrealDB logo
|
||||
println!("{LOGO}");
|
||||
}
|
||||
// Setup the cli options
|
||||
config::init(matches);
|
||||
let _ = config::CF.set(Config {
|
||||
strict,
|
||||
bind: listen_addresses.first().cloned().unwrap(),
|
||||
path,
|
||||
user,
|
||||
pass,
|
||||
crt: web.as_ref().and_then(|x| x.web_crt.clone()),
|
||||
key: web.as_ref().and_then(|x| x.web_key.clone()),
|
||||
});
|
||||
// Initiate environment
|
||||
env::init().await?;
|
||||
// Initiate master auth
|
||||
|
@ -32,19 +125,3 @@ async fn init_impl(matches: &clap::ArgMatches) -> Result<(), Error> {
|
|||
// All ok
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rust's default thread stack size of 2MiB doesn't allow sufficient recursion depth.
|
||||
fn with_enough_stack<T>(fut: impl Future<Output = T> + Send) -> T {
|
||||
let stack_size = 8 * 1024 * 1024;
|
||||
|
||||
// Stack frames are generally larger in debug mode.
|
||||
#[cfg(debug_assertions)]
|
||||
let stack_size = stack_size * 2;
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_stack_size(stack_size)
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(fut)
|
||||
}
|
||||
|
|
64
src/cli/validator/mod.rs
Normal file
64
src/cli/validator/mod.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) mod parser;
|
||||
|
||||
pub(crate) fn path_valid(v: &str) -> Result<String, String> {
|
||||
match v {
|
||||
"memory" => Ok(v.to_string()),
|
||||
v if v.starts_with("file:") => Ok(v.to_string()),
|
||||
v if v.starts_with("rocksdb:") => Ok(v.to_string()),
|
||||
v if v.starts_with("tikv:") => Ok(v.to_string()),
|
||||
v if v.starts_with("fdb:") => Ok(v.to_string()),
|
||||
_ => Err(String::from("Provide a valid database path parameter")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_exists(path: &str) -> Result<PathBuf, String> {
|
||||
let path = Path::new(path);
|
||||
if !*path.try_exists().as_ref().map_err(ToString::to_string)? {
|
||||
return Err(String::from("Ensure the file exists"));
|
||||
}
|
||||
if !path.is_file() {
|
||||
return Err(String::from("Ensure the path is a file"));
|
||||
}
|
||||
Ok(path.to_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn endpoint_valid(v: &str) -> Result<String, String> {
|
||||
fn split_endpoint(v: &str) -> (&str, &str) {
|
||||
match v {
|
||||
"memory" => ("mem", ""),
|
||||
v => match v.split_once("://") {
|
||||
Some(parts) => parts,
|
||||
None => v.split_once(':').unwrap_or_default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let scheme = split_endpoint(v).0;
|
||||
match scheme {
|
||||
"http" | "https" | "ws" | "wss" | "fdb" | "mem" | "rocksdb" | "file" | "tikv" => {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
_ => Err(String::from("Provide a valid database connection string")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_valid(v: &str) -> Result<String, String> {
|
||||
match v {
|
||||
v if v.ends_with(".db") => Ok(v.to_string()),
|
||||
v if v.starts_with("http://") => Ok(v.to_string()),
|
||||
v if v.starts_with("https://") => Ok(v.to_string()),
|
||||
"-" => Ok(v.to_string()),
|
||||
_ => Err(String::from("Provide a valid database connection string, or the path to a file")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn key_valid(v: &str) -> Result<String, String> {
|
||||
match v.len() {
|
||||
16 => Ok(v.to_string()),
|
||||
24 => Ok(v.to_string()),
|
||||
32 => Ok(v.to_string()),
|
||||
_ => Err(String::from("Ensure your database encryption key is 16, 24, or 32 bytes long")),
|
||||
}
|
||||
}
|
74
src/cli/validator/parser/env_filter.rs
Normal file
74
src/cli/validator/parser/env_filter.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use clap::builder::{NonEmptyStringValueParser, PossibleValue, TypedValueParser};
|
||||
use clap::error::{ContextKind, ContextValue, ErrorKind};
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CustomEnvFilter(pub EnvFilter);
|
||||
|
||||
impl Clone for CustomEnvFilter {
|
||||
fn clone(&self) -> Self {
|
||||
Self(EnvFilter::builder().parse(self.0.to_string()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CustomEnvFilterParser;
|
||||
|
||||
impl CustomEnvFilterParser {
|
||||
pub fn new() -> CustomEnvFilterParser {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedValueParser for CustomEnvFilterParser {
|
||||
type Value = CustomEnvFilter;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
let inner = NonEmptyStringValueParser::new();
|
||||
let v = inner.parse_ref(cmd, arg, value)?;
|
||||
let filter = (match v.as_str() {
|
||||
// Don't show any logs at all
|
||||
"none" => Ok(EnvFilter::default()),
|
||||
// Check if we should show all log levels
|
||||
"full" => Ok(EnvFilter::default().add_directive(Level::TRACE.into())),
|
||||
// Otherwise, let's only show errors
|
||||
"error" => Ok(EnvFilter::default().add_directive(Level::ERROR.into())),
|
||||
// Specify the log level for each code area
|
||||
"warn" | "info" | "debug" | "trace" => EnvFilter::builder()
|
||||
.parse(format!("error,surreal={v},surrealdb={v},surrealdb::txn=error")),
|
||||
// Let's try to parse the custom log level
|
||||
_ => EnvFilter::builder().parse(v),
|
||||
})
|
||||
.map_err(|e| {
|
||||
let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
|
||||
err.insert(ContextKind::Custom, ContextValue::String(e.to_string()));
|
||||
err.insert(
|
||||
ContextKind::InvalidValue,
|
||||
ContextValue::String("Provide a valid log filter configuration string".to_string()),
|
||||
);
|
||||
err
|
||||
})?;
|
||||
Ok(CustomEnvFilter(filter))
|
||||
}
|
||||
|
||||
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
|
||||
Some(Box::new(
|
||||
[
|
||||
PossibleValue::new("none"),
|
||||
PossibleValue::new("full"),
|
||||
PossibleValue::new("error"),
|
||||
PossibleValue::new("warn"),
|
||||
PossibleValue::new("info"),
|
||||
PossibleValue::new("debug"),
|
||||
PossibleValue::new("trace"),
|
||||
]
|
||||
.into_iter(),
|
||||
))
|
||||
}
|
||||
}
|
1
src/cli/validator/parser/mod.rs
Normal file
1
src/cli/validator/parser/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod env_filter;
|
|
@ -1,7 +1,7 @@
|
|||
use crate::env::release;
|
||||
use crate::err::Error;
|
||||
|
||||
pub fn init(_: &clap::ArgMatches) -> Result<(), Error> {
|
||||
pub fn init() -> Result<(), Error> {
|
||||
println!("{}", release());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ pub enum Error {
|
|||
#[error("There was a problem connecting with the storage engine")]
|
||||
InvalidStorage,
|
||||
|
||||
#[error("The operation is unsupported")]
|
||||
OperationUnsupported,
|
||||
|
||||
#[error("There was a problem with the database: {0}")]
|
||||
Db(#[from] SurrealError),
|
||||
|
||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -26,8 +26,26 @@ mod net;
|
|||
mod o11y;
|
||||
mod rpc;
|
||||
|
||||
use std::future::Future;
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
cli::init() // Initiate the command line
|
||||
// Initiate the command line
|
||||
with_enough_stack(cli::init())
|
||||
}
|
||||
|
||||
/// Rust's default thread stack size of 2MiB doesn't allow sufficient recursion depth.
|
||||
fn with_enough_stack<T>(fut: impl Future<Output = T> + Send) -> T {
|
||||
let stack_size = 8 * 1024 * 1024;
|
||||
|
||||
// Stack frames are generally larger in debug mode.
|
||||
#[cfg(debug_assertions)]
|
||||
let stack_size = stack_size * 2;
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_stack_size(stack_size)
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(fut)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
mod logger;
|
||||
mod tracers;
|
||||
|
||||
use crate::cli::validator::parser::env_filter::CustomEnvFilter;
|
||||
use tracing::Subscriber;
|
||||
use tracing_subscriber::{prelude::*, util::SubscriberInitExt};
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
use tracing_subscriber::{prelude::*, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Builder {
|
||||
log_level: String,
|
||||
log_level: Option<String>,
|
||||
filter: Option<CustomEnvFilter>,
|
||||
}
|
||||
|
||||
pub fn builder() -> Builder {
|
||||
|
@ -16,14 +19,30 @@ pub fn builder() -> Builder {
|
|||
impl Builder {
|
||||
/// Set the log level on the builder
|
||||
pub fn with_log_level(mut self, log_level: &str) -> Self {
|
||||
self.log_level = log_level.to_string();
|
||||
self.log_level = Some(log_level.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the filter on the builder
|
||||
pub fn with_filter(mut self, filter: EnvFilter) -> Self {
|
||||
self.filter = Some(CustomEnvFilter(filter));
|
||||
self
|
||||
}
|
||||
/// Build a dispatcher with the fmt subscriber (logs) and the chosen tracer subscriber
|
||||
pub fn build(self) -> Box<dyn Subscriber + Send + Sync + 'static> {
|
||||
Box::new(
|
||||
tracing_subscriber::registry().with(logger::new(self.log_level)).with(tracers::new()),
|
||||
)
|
||||
let registry = tracing_subscriber::registry();
|
||||
let registry = registry.with(self.filter.map(|filter| {
|
||||
tracing_subscriber::fmt::layer()
|
||||
.compact()
|
||||
.with_ansi(true)
|
||||
.with_span_events(FmtSpan::NONE)
|
||||
.with_writer(std::io::stderr)
|
||||
.with_filter(filter.0)
|
||||
.boxed()
|
||||
}));
|
||||
let registry = registry.with(self.log_level.map(logger::new));
|
||||
let registry = registry.with(tracers::new());
|
||||
Box::new(registry)
|
||||
}
|
||||
/// Build a dispatcher and set it as global
|
||||
pub fn init(self) {
|
||||
|
|
Loading…
Reference in a new issue