From b27d81400a30b6560e2867c0bb6a1e4fa1687f4f Mon Sep 17 00:00:00 2001 From: domchan <31119455+domechn@users.noreply.github.com> Date: Sat, 28 Oct 2023 12:00:00 -0500 Subject: [PATCH] support export configuration data (#130) --- src-tauri/Cargo.lock | 7 ++ src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 11 +++ src/components/data-management/index.css | 6 +- src/components/data-management/index.tsx | 34 +++++-- src/components/settings/index.tsx | 107 +++++++++++------------ src/middlelayers/configuration.ts | 35 ++++++-- src/middlelayers/data.ts | 53 +++++++++-- 8 files changed, 176 insertions(+), 78 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ac8df57..44cf630 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1992,6 +1992,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" @@ -4281,6 +4287,7 @@ dependencies = [ "coingecko", "lazy_static", "magic-crypt", + "md5", "okex", "rand 0.3.23", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d635a32..31a2087 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ tauri = {version = "1.2", features = ["app-all", "dialog-open", "dialog-save", " tokio = {version = "1", features = ["sync"] } uuid = "1.3.3" tauri-plugin-aptabase = "0.3" +md5 = "0.7.0" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5c6baa1..98460e5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -104,6 +104,16 @@ fn decrypt(data: String) -> Result { } } +#[cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] +#[tauri::command] +fn md5(data: String) -> Result { + let digest = md5::compute(data.as_bytes()); + Ok(format!("{:x}", digest)) +} + #[cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" @@ -160,6 +170,7 @@ fn main() { query_okex_balance, encrypt, decrypt, + md5, get_polybase_namespace, ]) .run(tauri::generate_context!()) diff --git a/src/components/data-management/index.css b/src/components/data-management/index.css index c6900d2..588d0e1 100644 --- a/src/components/data-management/index.css +++ b/src/components/data-management/index.css @@ -18,7 +18,7 @@ background-color: #a1a1a1; } -.dataManagement input { +.dataManagement input:not([type="checkbox"]) { width: 300px; height: 30px; margin-top: 20px; @@ -27,3 +27,7 @@ border: none; font-size: large; } + +.exportDataCheckbox { + width: 25px; +} diff --git a/src/components/data-management/index.tsx b/src/components/data-management/index.tsx index 2cd792e..1a104ce 100644 --- a/src/components/data-management/index.tsx +++ b/src/components/data-management/index.tsx @@ -21,7 +21,8 @@ import { } from "../../middlelayers/cloudsync"; import { LoadingContext } from "../../App"; import { timestampToDate } from "../../utils/date"; -import { trackEventWithClientID } from '../../utils/app' +import { trackEventWithClientID } from "../../utils/app"; +import Modal from "../common/modal"; const App = ({ onDataImported, @@ -41,6 +42,8 @@ const App = ({ const [lastSyncAt, setLastSyncAt] = useState(0); const [enableAutoSync, setEnableAutoSync] = useState(false); + const [exportConfiguration, setExportConfiguration] = useState(false); + useEffect(() => { onAuthStateUpdate((authState) => { setIsLogin(!!authState); @@ -103,7 +106,7 @@ const App = ({ } async function onExportDataClick() { - const exported = await exportHistoricalData(); + const exported = await exportHistoricalData(exportConfiguration); if (exported) { toast.success("export data successfully"); } @@ -141,7 +144,7 @@ const App = ({ try { await signIn(email, verificationCode); - trackEventWithClientID("sign_in") + trackEventWithClientID("sign_in"); } finally { if (signInRef.current) { signInRef.current!.disabled = false; @@ -319,7 +322,7 @@ const App = ({ style={{ marginTop: 10, }} - onClick={()=>syncDataBetweenCloudAndLocal()} + onClick={() => syncDataBetweenCloudAndLocal()} > Sync Data ( Beta ) @@ -330,7 +333,7 @@ const App = ({ backgroundColor: "#FF4500", color: "white", }} - onClick={()=>syncDataBetweenCloudAndLocal(true)} + onClick={() => syncDataBetweenCloudAndLocal(true)} > Hard Sync Data ( Beta ) @@ -351,6 +354,27 @@ const App = ({
+

Select Exported Data

+
+ setExportConfiguration(e.target.checked)} + /> + Export Configuration +
+
+ + Export Historical Data +
diff --git a/src/components/settings/index.tsx b/src/components/settings/index.tsx index 5ed2df2..749815a 100644 --- a/src/components/settings/index.tsx +++ b/src/components/settings/index.tsx @@ -21,6 +21,7 @@ const App = ({ }) => { const [version, setVersion] = useState("0.1.0"); const [isModalOpen, setIsModalOpen] = useState(false); + const [activeId, setActiveId] = useState("configuration"); const size = useWindowSize(); const [isSmallScreenAndSidecarActive, setIsSmallScreenAndSidecarActive] = useState(true); @@ -28,7 +29,8 @@ const App = ({ useEffect(() => { if (isModalOpen) { loadVersion(); - setIsSmallScreenAndSidecarActive(true) + + setIsSmallScreenAndSidecarActive(true); } }, [isModalOpen]); @@ -46,40 +48,6 @@ const App = ({ setIsModalOpen(false); } - function setActiveOnSidebarItem(activeId?: string) { - const allowedIds = ["configuration", "data"]; - const allowedContentIds = _(allowedIds) - .map((id) => `${id}Content`) - .value(); - const sidebarItems = document.getElementsByClassName("sidebar-item"); - const contentItems = document.getElementsByClassName("content-item"); - _.forEach(sidebarItems, (item) => { - if (allowedIds.includes(item.id)) { - item.classList.remove("active"); - } - }); - - _.forEach(contentItems, (item) => { - if (allowedContentIds.includes(item.id)) { - (item as any).style.display = "none"; - } - }); - if (!activeId) { - return - } - - const activeSidebarItem = document.getElementById(activeId); - if (activeSidebarItem) { - activeSidebarItem.classList.add("active"); - } - - const activeContentItem = document.getElementById(`${activeId}Content`); - - if (activeContentItem) { - activeContentItem.style.display = "block"; - } - } - function getSettingWidth() { const width = Math.floor(size.width ? size.width * 0.8 : 800); // keep it even @@ -91,13 +59,13 @@ const App = ({ function onConfigurationSidebarClick() { // add active class to the clicked item - setActiveOnSidebarItem("configuration"); - setIsSmallScreenAndSidecarActive(false) + setActiveId("configuration"); + setIsSmallScreenAndSidecarActive(false); } function onDataSidebarClick() { // add active class to the clicked item - setActiveOnSidebarItem("data"); - setIsSmallScreenAndSidecarActive(false) + setActiveId("data"); + setIsSmallScreenAndSidecarActive(false); } function _onConfigurationSave() { @@ -124,26 +92,40 @@ const App = ({
Configuration
-
+
Data
version: {version}
-
+
-
{ - setIsSmallScreenAndSidecarActive(true) - // clear active class - setActiveOnSidebarItem() - }}>{'< back'}
-
+
{ + setIsSmallScreenAndSidecarActive(true); + // clear active class + setActiveId(""); + }} + > + {"< back"} +
+
{ return getConfigurationById(cloudSyncFixId) } @@ -45,30 +50,34 @@ async function saveConfigurationById(id: string, cfg: string) { await db.execute(`INSERT OR REPLACE INTO configuration (id, data) VALUES (${id}, ?)`, [encrypted]) } +export async function exportConfigurationString(): Promise { + const model = await getConfigurationModelById(fixId) + return model?.data +} + async function getConfigurationById(id: string): Promise { - const db = await getDatabase() - const configurations = await db.select(`SELECT * FROM configuration where id = ${id}`) - if (configurations.length === 0) { - return undefined + const model = await getConfigurationModelById(id) + if (!model) { + return } - const cfg = configurations[0].data + const cfg = model.data // legacy logic if (!cfg.startsWith(prefix)) { - return configurations[0] + return model } // decrypt data return invoke("decrypt", { data: cfg }).then((res) => { return { - ...configurations[0], + ...model, data: res, } }).catch((err) => { if (err.includes("not ent")) { - return configurations[0] + return model } throw err }) @@ -101,3 +110,13 @@ export async function getClientIDConfiguration(): Promise { const model = await getConfigurationById(clientInfoFixId) return model?.data } + +async function getConfigurationModelById(id: string): Promise { + const db = await getDatabase() + const configurations = await db.select(`SELECT * FROM configuration where id = ${id}`) + if (configurations.length === 0) { + return undefined + } + + return configurations[0] +} diff --git a/src/middlelayers/data.ts b/src/middlelayers/data.ts index 077e3c5..b1e6afb 100644 --- a/src/middlelayers/data.ts +++ b/src/middlelayers/data.ts @@ -13,8 +13,16 @@ import { ASSETS_TABLE_NAME, queryHistoricalData } from './charts' import _ from 'lodash' import { save, open } from "@tauri-apps/api/dialog" import { writeTextFile, readTextFile } from "@tauri-apps/api/fs" -import { AssetModel } from './types' +import { AssetModel, HistoricalData } from './types' import { getDatabase } from './database' +import { exportConfigurationString, saveRawConfiguration } from './configuration' + +type ExportData = { + exportAt: string + configuration?: string + historicalData: Pick[] + md5: string +} export async function queryCoinPrices(symbols: string[]): Promise<{ [k: string]: number }> { return await invoke("query_coins_prices", { symbols }) @@ -50,7 +58,7 @@ async function loadPortfoliosByConfig(config: CexConfig & TokenConfig): Promise< return assets } -export async function exportHistoricalData(): Promise { +export async function exportHistoricalData(exportConfiguration = false): Promise { const filePath = await save({ filters: [ { @@ -66,9 +74,20 @@ export async function exportHistoricalData(): Promise { } const data = await queryHistoricalData(-1) - const content = JSON.stringify({ + + const exportAt = new Date().toISOString() + + const cfg = exportConfiguration ? await exportConfigurationString() : undefined + + const exportData = { + exportAt, historicalData: _.map(data, (obj) => _.omit(obj, "id")), - }) + configuration: cfg + } + const content = JSON.stringify({ + ...exportData, + md5: await invoke("md5", { data: JSON.stringify(exportData) }), + } as ExportData) // save to filePath await writeTextFile(filePath, content) @@ -89,7 +108,16 @@ export async function importHistoricalData(): Promise { } const contents = await readTextFile(selected as string) - const { historicalData } = JSON.parse(contents) as { historicalData: any[] } + const { exportAt, md5, configuration, historicalData } = JSON.parse(contents) as ExportData + + // !compatible with older versions logic ( before 0.3.3 ) + if (md5) { + // verify md5 + const currentMd5 = await invoke("md5", { data: JSON.stringify({ exportAt, historicalData, configuration }) }) + if (currentMd5 !== md5) { + throw new Error("invalid data, md5 check failed: errorCode 000") + } + } if (!historicalData || !_(historicalData).isArray() || historicalData.length === 0) { throw new Error("invalid data: errorCode 001") @@ -100,8 +128,20 @@ export async function importHistoricalData(): Promise { if (assets.length === 0) { throw new Error("no data need to be imported: errorCode 003") } - const requiredKeys = ["uuid", "createdAt", "symbol", "amount", "value", "price"] + // start to import + await saveHistoricalDataAssets(assets) + + // import configuration if exported + if (configuration) { + await saveRawConfiguration(configuration) + } + + return true +} + +async function saveHistoricalDataAssets(assets: AssetModel[]) { + const requiredKeys = ["uuid", "createdAt", "symbol", "amount", "value", "price"] _(assets).forEach((asset) => { _(requiredKeys).forEach(k => { if (!_(asset).has(k)) { @@ -123,5 +163,4 @@ export async function importHistoricalData(): Promise { const executeValues = _(assets as AssetModel[]).sortBy(a => new Date(a.createdAt).getTime()).reverse().map(a => _(keys).map(k => _(a).get(k)).value()).flatten().value() await db.execute(insertSql, executeValues) - return true }