1
//! BonsaiDb command line tools.
2

            
3
use std::ffi::OsString;
4
use std::fmt::Debug;
5
use std::path::PathBuf;
6

            
7
use bonsaidb_client::fabruic::Certificate;
8
use bonsaidb_client::AsyncClient;
9
use bonsaidb_core::async_trait::async_trait;
10
#[cfg(any(feature = "password-hashing", feature = "token-authentication"))]
11
use bonsaidb_core::connection::AsyncStorageConnection;
12
use bonsaidb_server::{Backend, CustomServer, NoBackend, ServerConfiguration};
13
use clap::{Parser, Subcommand};
14
use url::Url;
15

            
16
use crate::AnyServerConnection;
17

            
18
/// All available command line commands.
19
7
#[derive(Subcommand, Debug)]
20
pub enum Command<Cli: CommandLine> {
21
    /// Execute a BonsaiDb server command.
22
    #[clap(flatten)]
23
    Server(bonsaidb_server::cli::Command<Cli::Backend>),
24
    /// An external command.
25
    #[clap(flatten)]
26
    External(Cli::Subcommand),
27
}
28

            
29
impl<Cli> Command<Cli>
30
where
31
    Cli: CommandLine,
32
{
33
    /// Executes the command.
34
    // TODO add client builder insetad of server_url
35
7
    pub async fn execute(
36
7
        self,
37
7
        server_url: Option<Url>,
38
7
        pinned_certificate: Option<Certificate>,
39
7
        #[cfg(feature = "password-hashing")] username: Option<String>,
40
7
        #[cfg(feature = "token-authentication")] token_id: Option<u64>,
41
7
        mut cli: Cli,
42
7
    ) -> anyhow::Result<()> {
43
7
        match self {
44
4
            Command::Server(server) => {
45
4
                if server_url.is_some() {
46
                    anyhow::bail!("server url provided for local-only command.")
47
4
                }
48
4

            
49
20
                server.execute_on(cli.open_server().await?).await?;
50
            }
51
3
            Command::External(command) => {
52
3
                let connection = if let Some(server_url) = server_url {
53
                    // TODO how does custom API handling work here?
54
2
                    let mut client = AsyncClient::build(server_url);
55

            
56
2
                    if let Some(certificate) = pinned_certificate {
57
2
                        client = client.with_certificate(certificate);
58
2
                    }
59

            
60
2
                    AnyServerConnection::Networked(client.build()?)
61
                } else {
62
5
                    AnyServerConnection::Local(cli.open_server().await?)
63
                };
64

            
65
                #[cfg(feature = "password-hashing")]
66
3
                let connection = if let Some(username) = username {
67
                    let password = bonsaidb_local::cli::read_password_from_stdin(false)?;
68
                    connection
69
                        .authenticate_with_password(&username, password)
70
                        .await?
71
                } else {
72
3
                    connection
73
                };
74

            
75
                #[cfg(feature = "token-authentication")]
76
3
                let connection = if let Some(token_id) = token_id {
77
                    let token = bonsaidb_core::connection::SensitiveString(std::env::var(
78
                        "BONSAIDB_TOKEN_SECRET",
79
                    )?);
80
                    connection.authenticate_with_token(token_id, &token).await?
81
                } else {
82
3
                    connection
83
                };
84

            
85
4
                cli.execute(command, connection).await?;
86
            }
87
        }
88
6
        Ok(())
89
6
    }
90
}
91

            
92
/// The command line interface for `bonsaidb`.
93
7
#[derive(Parser, Debug)]
94
pub struct Args<Cli: CommandLine> {
95
    /// A url to a remote server.
96
    #[clap(long)]
97
    pub url: Option<Url>,
98
    /// A pinned certificate to use when connecting to `url`.
99
    #[clap(short = 'c', long)]
100
    pub pinned_certificate: Option<PathBuf>,
101
    /// A token ID to authenticate as before executing the command. Use
102
    /// environment variable `BONSAIDB_TOKEN_SECRET` to provide the
103
    #[cfg(feature = "token-authentication")]
104
    #[clap(long = "token", short = 't')]
105
    pub token_id: Option<u64>,
106
    /// A user to authenticate as before executing the command. The password
107
    /// will be prompted for over stdin. When writing a script for headless
108
    /// automation, token authentication should be preferred.
109
    #[cfg(feature = "password-hashing")]
110
    #[clap(long = "username", short = 'u')]
111
    pub username: Option<String>,
112
    /// The command to execute on the connection specified.
113
    #[clap(subcommand)]
114
    pub command: Command<Cli>,
115
}
116

            
117
impl<Cli: CommandLine> Args<Cli> {
118
    /// Executes the command.
119
7
    pub async fn execute(self, cli: Cli) -> anyhow::Result<()> {
120
7
        let pinned_certificate = if let Some(cert_path) = self.pinned_certificate {
121
2
            let bytes = tokio::fs::read(cert_path).await?;
122
2
            Some(Certificate::from_der(bytes)?)
123
        } else {
124
5
            None
125
        };
126
7
        self.command
127
7
            .execute(
128
7
                self.url,
129
7
                pinned_certificate,
130
7
                #[cfg(feature = "password-hashing")]
131
7
                self.username,
132
7
                #[cfg(feature = "token-authentication")]
133
7
                self.token_id,
134
7
                cli,
135
7
            )
136
43
            .await
137
6
    }
138
}
139

            
140
/// A command line interface that can be executed with either a remote or local
141
/// connection to a server.
142
#[async_trait]
143
pub trait CommandLine: Sized + Send + Sync {
144
    /// The Backend for this command line.
145
    type Backend: Backend;
146
    /// The [`Subcommand`] which is embedded next to the built-in BonsaiDb
147
    /// commands.
148
    type Subcommand: Subcommand + Send + Sync + Debug;
149

            
150
    /// Runs the command-line interface using command-line arguments from the
151
    /// environment.
152
    async fn run(self) -> anyhow::Result<()> {
153
        Args::<Self>::parse().execute(self).await
154
    }
155

            
156
    /// Runs the command-line interface using the specified list of arguments.
157
7
    async fn run_from<I, T>(self, itr: I) -> anyhow::Result<()>
158
7
    where
159
7
        I: IntoIterator<Item = T> + Send,
160
7
        T: Into<OsString> + Clone + Send,
161
7
    {
162
45
        Args::<Self>::parse_from(itr).execute(self).await
163
20
    }
164

            
165
    /// Returns a new server initialized based on the same configuration used
166
    /// for [`CommandLine`].
167
6
    async fn open_server(&mut self) -> anyhow::Result<CustomServer<Self::Backend>> {
168
30
        Ok(CustomServer::<Self::Backend>::open(self.configuration().await?).await?)
169
18
    }
170

            
171
    /// Returns the server configuration to use when initializing a local server.
172
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration<Self::Backend>>;
173

            
174
    /// Execute the command on `connection`.
175
    async fn execute(
176
        &mut self,
177
        command: Self::Subcommand,
178
        connection: AnyServerConnection<Self::Backend>,
179
    ) -> anyhow::Result<()>;
180
}
181

            
182
#[async_trait]
183
impl CommandLine for NoBackend {
184
    type Backend = Self;
185
    type Subcommand = NoSubcommand;
186

            
187
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration> {
188
        Ok(ServerConfiguration::default())
189
    }
190

            
191
    async fn execute(
192
        &mut self,
193
        command: Self::Subcommand,
194
        _connection: AnyServerConnection<Self>,
195
    ) -> anyhow::Result<()> {
196
        match command {}
197
    }
198
}
199

            
200
/// Runs the command-line interface with only the built-in commands, using
201
/// `configuration` to launch a server if running a local command.
202
pub async fn run<B: Backend>(configuration: ServerConfiguration<B>) -> anyhow::Result<()> {
203
    Args::parse()
204
        .execute(NoCommandLine::<B> {
205
            configuration: Some(configuration),
206
        })
207
        .await
208
}
209

            
210
#[derive(Debug)]
211
struct NoCommandLine<B: Backend> {
212
    configuration: Option<ServerConfiguration<B>>,
213
}
214

            
215
#[async_trait]
216
impl<B: Backend> CommandLine for NoCommandLine<B> {
217
    type Backend = B;
218
    type Subcommand = NoSubcommand;
219

            
220
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration<B>> {
221
        self.configuration
222
            .take()
223
            .ok_or_else(|| anyhow::anyhow!("configuration already consumed"))
224
    }
225

            
226
    async fn execute(
227
        &mut self,
228
        command: Self::Subcommand,
229
        _connection: AnyServerConnection<B>,
230
    ) -> anyhow::Result<()> {
231
        match command {}
232
    }
233
}
234

            
235
/// A [`Subcommand`] implementor that has no options.
236
#[derive(clap::Subcommand, Debug)]
237
pub enum NoSubcommand {}