Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a CLI feature to backup the SQLite DB #4906

Merged
merged 2 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 11 additions & 21 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use crate::{
http_client::make_http_request,
mail,
util::{
container_base_image, format_naive_datetime_local, get_display_size, is_running_in_container, NumberOrString,
container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version,
is_running_in_container, NumberOrString,
},
CONFIG, VERSION,
};
Expand Down Expand Up @@ -575,11 +576,6 @@ async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -
org.delete(&mut conn).await
}

#[derive(Deserialize)]
struct WebVaultVersion {
version: String,
}

#[derive(Deserialize)]
struct GitRelease {
tag_name: String,
Expand Down Expand Up @@ -679,18 +675,6 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
use chrono::prelude::*;
use std::net::ToSocketAddrs;

// Get current running versions
let web_vault_version: WebVaultVersion =
match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "vw-version.json")) {
Ok(s) => serde_json::from_str(&s)?,
_ => match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) {
Ok(s) => serde_json::from_str(&s)?,
_ => WebVaultVersion {
version: String::from("Version file missing"),
},
},
};

// Execute some environment checks
let running_within_container = is_running_in_container();
let has_http_access = has_http_access().await;
Expand All @@ -710,13 +694,16 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)

let ip_header_name = &ip_header.0.unwrap_or_default();

// Get current running versions
let web_vault_version = get_web_vault_version();

let diagnostics_json = json!({
"dns_resolved": dns_resolved,
"current_release": VERSION,
"latest_release": latest_release,
"latest_commit": latest_commit,
"web_vault_enabled": &CONFIG.web_vault_enabled(),
"web_vault_version": web_vault_version.version.trim_start_matches('v'),
"web_vault_version": web_vault_version,
"latest_web_build": latest_web_build,
"running_within_container": running_within_container,
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
Expand Down Expand Up @@ -765,9 +752,12 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
}

#[post("/config/backup_db")]
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> EmptyResult {
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult<String> {
if *CAN_BACKUP {
backup_database(&mut conn).await
match backup_database(&mut conn).await {
Ok(f) => Ok(format!("Backup to '{f}' was successful")),
Err(e) => err!(format!("Backup was unsuccessful {e}")),
}
} else {
err!("Can't back up current DB (Only SQLite supports this feature)");
}
Expand Down
22 changes: 15 additions & 7 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,23 +368,31 @@ pub mod models;

/// Creates a back-up of the sqlite database
/// MySQL/MariaDB and PostgreSQL are not supported.
pub async fn backup_database(conn: &mut DbConn) -> Result<(), Error> {
pub async fn backup_database(conn: &mut DbConn) -> Result<String, Error> {
db_run! {@raw conn:
postgresql, mysql {
let _ = conn;
err!("PostgreSQL and MySQL/MariaDB do not support this backup feature");
}
sqlite {
use std::path::Path;
let db_url = CONFIG.database_url();
let db_path = Path::new(&db_url).parent().unwrap().to_string_lossy();
let file_date = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
diesel::sql_query(format!("VACUUM INTO '{db_path}/db_{file_date}.sqlite3'")).execute(conn)?;
Ok(())
backup_sqlite_database(conn)
}
}
}

#[cfg(sqlite)]
pub fn backup_sqlite_database(conn: &mut diesel::sqlite::SqliteConnection) -> Result<String, Error> {
use diesel::RunQueryDsl;
let db_url = CONFIG.database_url();
let db_path = std::path::Path::new(&db_url).parent().unwrap();
let backup_file = db_path
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
.to_string_lossy()
.into_owned();
diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?;
Ok(backup_file)
}

/// Get the SQL Server version
pub async fn get_sql_server_version(conn: &mut DbConn) -> String {
db_run! {@raw conn:
Expand Down
65 changes: 60 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use std::{
use tokio::{
fs::File,
io::{AsyncBufReadExt, BufReader},
signal::unix::SignalKind,
};

#[macro_use]
Expand Down Expand Up @@ -97,10 +98,12 @@ USAGE:

FLAGS:
-h, --help Prints help information
-v, --version Prints the app version
-v, --version Prints the app and web-vault version

COMMAND:
hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN
backup Create a backup of the SQLite database
You can also send the USR1 signal to trigger a backup

PRESETS: m= t= p=
bitwarden (default) 64MiB, 3 Iterations, 4 Threads
Expand All @@ -115,11 +118,13 @@ fn parse_args() {
let version = VERSION.unwrap_or("(Version info from Git not present)");

if pargs.contains(["-h", "--help"]) {
println!("vaultwarden {version}");
println!("Vaultwarden {version}");
print!("{HELP}");
exit(0);
} else if pargs.contains(["-v", "--version"]) {
println!("vaultwarden {version}");
let web_vault_version = util::get_web_vault_version();
println!("Vaultwarden {version}");
println!("Web-Vault {web_vault_version}");
exit(0);
}

Expand Down Expand Up @@ -174,13 +179,47 @@ fn parse_args() {
argon2_timer.elapsed()
);
} else {
error!("Unable to generate Argon2id PHC hash.");
println!("Unable to generate Argon2id PHC hash.");
exit(1);
}
} else if command == "backup" {
match backup_sqlite() {
Ok(f) => {
println!("Backup to '{f}' was successful");
exit(0);
}
Err(e) => {
println!("Backup failed. {e:?}");
exit(1);
}
}
}
exit(0);
}
}

fn backup_sqlite() -> Result<String, Error> {
#[cfg(sqlite)]
{
use crate::db::{backup_sqlite_database, DbConnType};
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) {
use diesel::Connection;
let url = crate::CONFIG.database_url();

// Establish a connection to the sqlite database
let mut conn = diesel::sqlite::SqliteConnection::establish(&url)?;
let backup_file = backup_sqlite_database(&mut conn)?;
Ok(backup_file)
} else {
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
}
}
#[cfg(not(sqlite))]
{
err_silent!("The 'sqlite' feature is not enabled. Backups only works for SQLite databases")
}
}

fn launch_info() {
println!(
"\
Expand Down Expand Up @@ -346,7 +385,7 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
}
#[cfg(not(windows))]
{
const SIGHUP: i32 = tokio::signal::unix::SignalKind::hangup().as_raw_value();
const SIGHUP: i32 = SignalKind::hangup().as_raw_value();
let path = Path::new(&log_file);
logger = logger.chain(fern::log_reopen1(path, [SIGHUP])?);
}
Expand Down Expand Up @@ -560,6 +599,22 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
CONFIG.shutdown();
});

#[cfg(unix)]
{
tokio::spawn(async move {
let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap();
loop {
// If we need more signals to act upon, we might want to use select! here.
// With only one item to listen for this is enough.
let _ = signal_user1.recv().await;
match backup_sqlite() {
Ok(f) => info!("Backup to '{f}' was successful"),
Err(e) => error!("Backup failed. {e:?}"),
}
}
});
}

let _ = instance.launch().await?;

info!("Vaultwarden process exited!");
Expand Down
22 changes: 22 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,28 @@ pub fn container_base_image() -> &'static str {
}
}

#[derive(Deserialize)]
struct WebVaultVersion {
version: String,
}

pub fn get_web_vault_version() -> String {
let version_files = [
format!("{}/vw-version.json", CONFIG.web_vault_folder()),
format!("{}/version.json", CONFIG.web_vault_folder()),
];

for version_file in version_files {
if let Ok(version_str) = std::fs::read_to_string(&version_file) {
if let Ok(version) = serde_json::from_str::<WebVaultVersion>(&version_str) {
return String::from(version.version.trim_start_matches('v'));
}
}
}

String::from("Version file missing")
}

//
// Deserialization methods
//
Expand Down
Loading