bonsaidb_local/
cli.rs

1use std::io;
2use std::path::PathBuf;
3
4use clap::Subcommand;
5
6use crate::config::StorageConfiguration;
7use crate::{Error, Storage};
8
9/// Commands for administering the bonsaidb server.
10pub mod admin;
11/// Commands for querying the schemas.
12pub mod schema;
13
14/// Commands operating on local database storage.
15#[derive(Subcommand, Debug)]
16pub enum StorageCommand {
17    /// Back up the storage.
18    #[clap(subcommand)]
19    Backup(Location),
20    /// Restore the storage from backup.
21    #[clap(subcommand)]
22    Restore(Location),
23    /// Executes an admin command.
24    #[clap(subcommand)]
25    Admin(admin::Command),
26    /// Executes a schema query.
27    Schema(schema::Command),
28}
29
30/// A backup location.
31#[derive(Subcommand, Debug)]
32pub enum Location {
33    /// A filesystem-based backup location.
34    Path {
35        /// The path to the backup directory.
36        path: PathBuf,
37    },
38}
39
40impl StorageCommand {
41    /// Executes the command after opening a [`Storage`] instance using `config`.
42    pub fn execute(self, config: StorageConfiguration) -> Result<(), Error> {
43        let storage = Storage::open(config)?;
44        self.execute_on(&storage)
45    }
46
47    /// Executes the command on `storage`.
48    pub fn execute_on(self, storage: &Storage) -> Result<(), Error> {
49        match self {
50            StorageCommand::Backup(location) => location.backup(storage),
51            StorageCommand::Restore(location) => location.restore(storage),
52            StorageCommand::Admin(admin) => admin.execute(storage),
53            StorageCommand::Schema(schema) => schema.execute(storage),
54        }
55    }
56
57    /// Executes the command on `storage`.
58    #[cfg(feature = "async")]
59    pub async fn execute_on_async(self, storage: &crate::AsyncStorage) -> Result<(), Error> {
60        match self {
61            StorageCommand::Backup(location) => location.backup_async(storage).await,
62            StorageCommand::Restore(location) => location.restore_async(storage).await,
63            StorageCommand::Admin(admin) => admin.execute_async(storage).await,
64            StorageCommand::Schema(schema) => schema.execute_async(storage).await,
65        }
66    }
67}
68
69impl Location {
70    /// Backs-up `storage` to `self`.
71    pub fn backup(&self, storage: &Storage) -> Result<(), Error> {
72        match self {
73            Location::Path { path } => storage.backup(path),
74        }
75    }
76
77    /// Restores `storage` from `self`.
78    pub fn restore(&self, storage: &Storage) -> Result<(), Error> {
79        match self {
80            Location::Path { path } => storage.restore(path),
81        }
82    }
83
84    /// Backs-up `storage` to `self`.
85    #[cfg(feature = "async")]
86    pub async fn backup_async(&self, storage: &crate::AsyncStorage) -> Result<(), Error> {
87        match self {
88            Location::Path { path } => storage.backup(path.clone()).await,
89        }
90    }
91
92    /// Restores `storage` from `self`.
93    #[cfg(feature = "async")]
94    pub async fn restore_async(&self, storage: &crate::AsyncStorage) -> Result<(), Error> {
95        match self {
96            Location::Path { path } => storage.restore(path.clone()).await,
97        }
98    }
99}
100
101/// Reads a password from stdin, wrapping the result in a
102/// [`SensitiveString`](bonsaidb_core::connection::SensitiveString). If
103/// `confirm` is true, the user will be prompted to enter the password a second
104/// time, and the passwords will be compared to ensure they are the same before
105/// returning.
106#[cfg(feature = "password-hashing")]
107pub fn read_password_from_stdin(
108    confirm: bool,
109) -> Result<bonsaidb_core::connection::SensitiveString, ReadPasswordError> {
110    let password = read_sensitive_input_from_stdin("Enter Password:")?;
111    if confirm {
112        let confirmed = read_sensitive_input_from_stdin("Re-enter the same password:")?;
113        if password != confirmed {
114            return Err(ReadPasswordError::PasswordConfirmationFailed);
115        }
116    }
117    Ok(password)
118}
119
120/// An error that may occur from reading a password from the terminal.
121#[cfg(feature = "password-hashing")]
122#[derive(thiserror::Error, Debug)]
123pub enum ReadPasswordError {
124    /// The password input was cancelled.
125    #[error("password input cancelled")]
126    Cancelled,
127    /// The confirmation password did not match the originally entered password.
128    #[error("password confirmation did not match")]
129    PasswordConfirmationFailed,
130    /// An error occurred interacting with the terminal.
131    #[error("io error: {0}")]
132    Io(#[from] io::Error),
133}
134
135#[cfg(feature = "password-hashing")]
136fn read_sensitive_input_from_stdin(
137    prompt: &str,
138) -> Result<bonsaidb_core::connection::SensitiveString, ReadPasswordError> {
139    use std::io::stdout;
140
141    use crossterm::cursor::MoveToColumn;
142    use crossterm::terminal::{Clear, ClearType};
143    use crossterm::ExecutableCommand;
144
145    println!("{prompt} (input Enter or Return when done, or Escape to cancel)");
146
147    crossterm::terminal::enable_raw_mode()?;
148    let password = read_password_loop();
149    drop(
150        stdout()
151            .execute(MoveToColumn(1))?
152            .execute(Clear(ClearType::CurrentLine)),
153    );
154    crossterm::terminal::disable_raw_mode()?;
155    if let Some(password) = password? {
156        println!("********");
157        Ok(password)
158    } else {
159        Err(ReadPasswordError::Cancelled)
160    }
161}
162
163#[cfg(feature = "password-hashing")]
164fn read_password_loop() -> io::Result<Option<bonsaidb_core::connection::SensitiveString>> {
165    const ESCAPE: u8 = 27;
166    const BACKSPACE: u8 = 127;
167    const CANCEL: u8 = 3;
168    const EOF: u8 = 4;
169    const CLEAR_BEFORE_CURSOR: u8 = 21;
170
171    use std::io::Read;
172
173    use crossterm::cursor::{MoveLeft, MoveToColumn};
174    use crossterm::style::Print;
175    use crossterm::terminal::{Clear, ClearType};
176    use crossterm::ExecutableCommand;
177    let mut stdin = std::io::stdin();
178    let mut stdout = std::io::stdout();
179    let mut buffer = [0; 1];
180    let mut password = bonsaidb_core::connection::SensitiveString::default();
181    loop {
182        if stdin.read(&mut buffer)? == 0 {
183            return Ok(Some(password));
184        }
185        match buffer[0] {
186            ESCAPE | CANCEL => return Ok(None),
187            BACKSPACE => {
188                password.pop();
189                stdout
190                    .execute(MoveLeft(1))?
191                    .execute(Clear(ClearType::UntilNewLine))?;
192            }
193            CLEAR_BEFORE_CURSOR => {
194                password.clear();
195                stdout
196                    .execute(MoveToColumn(1))?
197                    .execute(Clear(ClearType::CurrentLine))?;
198            }
199            b'\n' | b'\r' | EOF => {
200                return Ok(Some(password));
201            }
202            other if other.is_ascii_control() => {}
203            other => {
204                password.push(other as char);
205                stdout.execute(Print('*'))?;
206            }
207        }
208    }
209}