1
use std::io;
2
use std::path::PathBuf;
3

            
4
use clap::Subcommand;
5

            
6
use crate::config::StorageConfiguration;
7
use crate::{Error, Storage};
8

            
9
/// Commands for administering the bonsaidb server.
10
pub mod admin;
11
/// Commands for querying the schemas.
12
pub mod schema;
13

            
14
/// Commands operating on local database storage.
15
1224
#[derive(Subcommand, Debug)]
16
pub 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
648
#[derive(Subcommand, Debug)]
32
pub enum Location {
33
    /// A filesystem-based backup location.
34
    Path {
35
        /// The path to the backup directory.
36
        path: PathBuf,
37
    },
38
}
39

            
40
impl 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
72
    pub async fn execute_on_async(self, storage: &crate::AsyncStorage) -> Result<(), Error> {
60
2
        match self {
61
1
            StorageCommand::Backup(location) => location.backup_async(storage).await,
62
1
            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
2
    }
67
}
68

            
69
impl 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
36
    pub async fn backup_async(&self, storage: &crate::AsyncStorage) -> Result<(), Error> {
87
1
        match self {
88
1
            Location::Path { path } => storage.backup(path.clone()).await,
89
        }
90
1
    }
91

            
92
    /// Restores `storage` from `self`.
93
    #[cfg(feature = "async")]
94
36
    pub async fn restore_async(&self, storage: &crate::AsyncStorage) -> Result<(), Error> {
95
1
        match self {
96
1
            Location::Path { path } => storage.restore(path.clone()).await,
97
        }
98
1
    }
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")]
107
pub 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)]
123
pub 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")]
136
fn 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")]
164
fn 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
}