Skip to content

Commit

Permalink
feat: export internal database
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewinci committed Nov 30, 2022
1 parent e1dda33 commit 6a49dae
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ env_logger = { version = "0.10.0" }
async-trait = "0.1.59"
num-bigint = "0.4"
rust_decimal = "1.27"
rusqlite = { version = "0.28.0", features = ["bundled"] }
rusqlite = { version = "0.28.0", features = ["bundled", "backup"] }
rust-keystore = { git = "https://github.com/andrewinci/rust-keystore", features = ["p12"], tag = "v0.1.2" }
r2d2 = "0.8.10"
r2d2_sqlite = "0.21.0"
Expand Down
3 changes: 1 addition & 2 deletions src-tauri/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ mod notification;
pub mod schema_registry;
mod state;
mod types;

// pub use notification::notify_error;
pub mod utils;
pub use state::AppState;
10 changes: 10 additions & 0 deletions src-tauri/src/api/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use log::debug;

use super::error::Result;
use super::AppState;

#[tauri::command]
pub async fn export_datastore(cluster_id: &str, output_path: &str, state: tauri::State<'_, AppState>) -> Result<()> {
debug!("Start export database");
Ok(state.get_cluster(cluster_id).await.store.export_db(output_path)?)
}
46 changes: 44 additions & 2 deletions src-tauri/src/lib/record_store/sqlite_store.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use core::time;

use crate::lib::{types::ParsedKafkaRecord, Error, Result};
use log::debug;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{named_params, OpenFlags};
use rusqlite::{backup::Backup, named_params, Connection, OpenFlags};

pub struct Query {
pub cluster_id: String,
Expand Down Expand Up @@ -156,6 +159,23 @@ impl SqliteStore {
SqliteStore { pool }
}

pub fn export_db(&self, output_path: &str) -> Result<()> {
let src = self.pool.get().unwrap();
let mut dst = Connection::open(output_path)?;
let backup = Backup::new(&src, &mut dst)?;
backup.run_to_completion(
1000,
time::Duration::from_millis(100),
Some(|p| {
debug!(
"Export in progress: {}%",
100f32 * ((p.pagecount - p.remaining) as f32 / p.pagecount as f32)
)
}),
)?;
Ok(())
}

fn parse_query(query: &Query) -> String {
let Query {
cluster_id,
Expand Down Expand Up @@ -185,12 +205,34 @@ impl SqliteStore {

#[cfg(test)]
mod tests {
use std::{sync::Arc, thread::spawn, time::Instant};
use std::{env::temp_dir, sync::Arc, thread::spawn, time::Instant};

use crate::lib::{record_store::sqlite_store::Query, types::ParsedKafkaRecord};

use super::{RecordStore, SqliteStore};

fn get_test_db_path() -> String {
let mut dir = temp_dir();
dir.push("test.db");
dir.to_str().unwrap().into()
}

#[tokio::test]
async fn test_export_database() {
// arrange
let test_db_path = get_test_db_path();
let (cluster_id, topic_name) = ("cluster_id_example", "topic_name_example");
let db = SqliteStore::new();
db.create_topic_table(cluster_id, topic_name).unwrap();
let test_record = get_test_record(topic_name, 0);
db.insert_record(cluster_id, topic_name, &test_record).unwrap();
// act
let res = db.export_db(&test_db_path);
// assert
assert!(res.is_ok());
//todo: validate DB content
}

#[tokio::test]
async fn test_create_table() {
let db = SqliteStore::new();
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::api::{
configuration::{get_configuration, write_configuration},
consumer::{export_records, get_consumer_state, get_records_page, start_consumer, stop_consumer},
schema_registry::{delete_subject, delete_subject_version, get_subject, list_subjects, post_schema},
utils::export_datastore,
};
use api::AppState;
use tauri::Manager;
Expand All @@ -22,6 +23,8 @@ fn main() {
log_active_user();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
// utils
export_datastore,
// consumer
start_consumer,
stop_consumer,
Expand Down
41 changes: 40 additions & 1 deletion webapp/pages/settings/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
import { Checkbox, Container, Select, Stack, Center } from "@mantine/core";
import { Checkbox, Container, Select, Stack, Center, Button } from "@mantine/core";
import { useSessionStorage } from "@mantine/hooks";
import { save } from "@tauri-apps/api/dialog";
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { PageHeader } from "../../components";
import { AppTheme } from "../../models";
import { useNotifications } from "../../providers";
import { useUserSettings } from "../../providers/user-settings-provider";
import { exportDatastore } from "../../tauri/utils";

export const Settings = () => {
const { clusterId } = useParams();
const { success } = useNotifications();
const { userSettings, setUserSettings } = useUserSettings();
const clusterName = useMemo(
() => userSettings.clusters.find((c) => c.id == clusterId)?.name,
[userSettings, clusterId]
);
const [exportStatus, setExportStatus] = useSessionStorage({
key: `export-database-${clusterName}`,
defaultValue: { inProgress: false },
});

const exportDB = async () => {
const outputPath = await save({
title: "Save SQLite DB",
defaultPath: `${clusterName?.replace(" ", "_")}.db`,
filters: [{ name: clusterName ?? "db", extensions: ["db"] }],
});
if (!clusterId || !outputPath) return;
setExportStatus((_) => ({ inProgress: true }));
try {
await exportDatastore(clusterId, outputPath);
success(`Database for ${clusterName} successfully exported to ${outputPath}`);
} finally {
setExportStatus((_) => ({ inProgress: false }));
}
};

return (
<Container>
Expand Down Expand Up @@ -35,6 +67,13 @@ export const Settings = () => {
checked={userSettings.useRegex}
onChange={(c) => setUserSettings((s) => ({ ...s, useRegex: c.target.checked }))}
/>
{clusterId && (
<>
<Button loading={exportStatus.inProgress} onClick={exportDB}>
Export sqlite DB
</Button>
</>
)}
</Stack>
</Center>
</Container>
Expand Down
15 changes: 15 additions & 0 deletions webapp/tauri/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { invoke } from "@tauri-apps/api";
import { addNotification } from "../providers";

export const exportDatastore = async (clusterId: string, outputPath: string): Promise<void> => {
try {
return await invoke<void>("export_datastore", {
clusterId,
outputPath,
});
} catch (err) {
addNotification({ type: "error", title: "Database export failed", description: "" });
console.log(err);
throw err;
}
};

0 comments on commit 6a49dae

Please sign in to comment.