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

Migrate data from umami old version to new version #2215

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
9 changes: 9 additions & 0 deletions apps/desktop-e2e/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {};

declare global {
interface Window {
electronAPI?: {
onBackupData: (fn: (event: any, data?: Record<string, string>) => void) => void;
};
}
}
8 changes: 6 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@
"@umami/state": "workspace:^",
"@umami/test-utils": "workspace:^",
"@umami/tezos": "workspace:^",
"@umami/utils": "workspace:^",
"@umami/typescript-config": "workspace:^",
"@umami/tzkt": "workspace:^",
"@umami/utils": "workspace:^",
"@vitejs/plugin-react": "^4.3.4",
"babel-jest": "^29.7.0",
"bignumber.js": "^9.1.2",
Expand Down Expand Up @@ -156,6 +156,10 @@
},
"packageManager": "[email protected]",
"dependencies": {
"electron-updater": "6.3.9"
"electron-log": "^5.2.4",
"electron-updater": "6.3.9",
"level": "^9.0.0",
"level-supports": "^6.0.0",
"level-transcoder": "^1.0.1"
}
}
131 changes: 127 additions & 4 deletions apps/desktop/public/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ const path = require("path");
const url = require("url");
const process = require("process");
const { autoUpdater } = require("electron-updater");
const { Level } = require("level");
const log = require("electron-log");
const fs = require("fs");

const APP_PROTOCOL = "app";
const APP_HOST = "assets";

// backupData is used to store the backup data from the previous version of the app
let backupData;

const appURL = app.isPackaged
? url.format({
pathname: `${APP_HOST}/index.html`,
Expand All @@ -26,6 +33,109 @@ protocol.registerSchemesAsPrivileged([
},
]);

// Configure electron-log
log.transports.file.resolvePathFn = () => path.join(app.getPath("userData"), "umami-desktop.log");

async function createBackupFromPrevDB() {
const dbPath = path.normalize(path.join(app.getPath("userData"), "Local Storage", "leveldb"));
const backupPath = path.normalize(
path.join(app.getPath("userData"), "Local Storage", "backup_leveldb.json")
);

if (fs.existsSync(backupPath)) {
console.log("Backup file already exists. Skipping migration.");
return;
}

if (!fs.existsSync(dbPath)) {
log.info("LevelDB database not found at path. Code:EM01", dbPath);
return;
}

const db = new Level(dbPath);
await db.open();

try {
const storage = {};

// Function to clean up the string (removing non-printable chars)
function cleanString(str) {
str = str.replace(/[\x00\x01\x17\x10\x0f]/g, "");

return str;
}

const KEYS_TO_MIGRATE = ["_file://\x00\x01persist:accounts", "_file://\x00\x01persist:root"];
const ROOT_KEYS_TO_MIGRATE = [
"batches",
"beacon",
"networks",
"contacts",
"protocolSettings",
"_persist",
];

const extractKeys = json => {
const regexp = /"([^"]+)":("[^"\\]*(?:\\.[^"\\]*)*"|{[^}]+})/g;

const result = {};
const matches = json.matchAll(regexp);

for (const [_, key, value] of matches) {
if (ROOT_KEYS_TO_MIGRATE.includes(key)) {
try {
// Try to parse the value if it's a valid JSON
result[key] = JSON.parse(value);
} catch {
// If parsing fails, store as raw string
result[key] = value.replace(/^"|"$/g, "");
}
}
}

return result;
};

for await (const [_key, value] of db.iterator()) {
if (KEYS_TO_MIGRATE.includes(_key)) {
let cleanedValue = cleanString(value);

const key = _key.includes("_file://\x00\x01persist:root")
? "persist:root"
: "persist:accounts";

try {
storage[key] = JSON.parse(cleanedValue);
} catch (_) {
// Store as raw value if JSON parsing fails
storage[key] = cleanedValue;
}
}
}

const preparedStorage = {
...storage,
"persist:root": extractKeys(storage["persist:root"]),
};

backupData = preparedStorage;

// Write storage object to JSON file
try {
fs.writeFileSync(backupPath, JSON.stringify(preparedStorage, null, 2), "utf-8");
log.info("Backup successfully created at:", backupPath);
} catch (err) {
log.error("Error during LevelDB backup creation. Code:EM2.", err);
}
} catch (err) {
log.error("Error during key migration. Code:EM4.", err);
} finally {
db.close().catch(err => {
log.error("Error closing the database. Code:EM5", err);
});
}
}

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
Expand Down Expand Up @@ -129,9 +239,14 @@ function createWindow() {
});

mainWindow.loadURL(appURL);

mainWindow.once("ready-to-show", () => {
mainWindow.show();

if (backupData !== undefined) {
mainWindow.webContents.send("backupData", backupData);
}

if (deeplinkURL) {
mainWindow.webContents.send("deeplinkURL", deeplinkURL);
deeplinkURL = null;
Expand Down Expand Up @@ -174,7 +289,7 @@ function start() {
try {
autoUpdater.checkForUpdatesAndNotify();
} catch (e) {
console.log(e);
log.error(e);
}

if (!app.isDefaultProtocolClient("umami")) {
Expand All @@ -198,7 +313,7 @@ function start() {
}
mainWindow.focus();
// Protocol handler for win32
// argv: An array of the second instances (command line / deep linked) arguments
// argv: An array of the second instance's (command line / deep linked) arguments
if (process.platform === "win32" || process.platform === "linux") {
// Protocol handler for windows & linux
const index = argv.findIndex(arg => arg.startsWith("umami://"));
Expand All @@ -223,7 +338,15 @@ function start() {
// This method will be called when Electron has finished its initialization and
// is ready to create the browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow);
app.whenReady().then(async () => {
// Execute createBackupFromPrevDB at the beginning
try {
await createBackupFromPrevDB();
createWindow();
} catch (error) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about putting createWindow outside the try and catch block. we dont want people to encouter error if the restoration of backup fails. We just want them to see clear welcome screen.

log.error("Error has occured while initialising the app", error);
}
});

app.on("activate", function () {
// On macOS it's common to re-create a window in the app when the
Expand All @@ -236,7 +359,7 @@ function start() {
// Send event to UI when app update is ready to be installed.
// If the update installation won't be triggered by the user, it will be applied the next time the app starts.
autoUpdater.on("update-downloaded", event => {
console.log(`Umami update ${event.version} downloaded and ready to be installed`, url);
log.info(`Umami update ${event.version} downloaded and ready to be installed`, url);
return mainWindow.webContents.send("app-update-downloaded");
});

Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/public/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
// Notify UI if app update is available to be installed.
onAppUpdateDownloaded: callback => ipcRenderer.on("app-update-downloaded", callback),

// handle the backupData send in electron.js
onBackupData: callback => ipcRenderer.on("backupData", callback),

// Notify Electron that app update should be installed.
installAppUpdateAndQuit: () => ipcRenderer.send("install-app-update"),

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ declare global {
onDeeplink: (callback: (url: string) => void) => void;
onAppUpdateDownloaded: (callback: () => void) => void;
installAppUpdateAndQuit: () => void;
onBackupData: (fn: (event: any, data?: Record<string, string>) => void) => void;
};
}
}
9 changes: 9 additions & 0 deletions apps/web/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {};

declare global {
interface Window {
electronAPI?: {
onBackupData: (fn: (event: any, data?: Record<string, string>) => void) => void;
};
}
}
9 changes: 9 additions & 0 deletions packages/components/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {};

declare global {
interface Window {
electronAPI?: {
onBackupData: (fn: (event: any, data?: Record<string, string>) => void) => void;
};
}
}
9 changes: 9 additions & 0 deletions packages/data-polling/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {};

declare global {
interface Window {
electronAPI?: {
onBackupData: (fn: (event: any, data?: Record<string, string>) => void) => void;
};
}
}
8 changes: 8 additions & 0 deletions packages/state/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ module.exports = {
parser: "@typescript-eslint/parser",
tsconfigRootDir: __dirname,
},
overrides: [
{
files: ["*.ts", "*.tsx"],
rules: {
"import/no-unused-modules": "off",
},
},
],
};
9 changes: 9 additions & 0 deletions packages/state/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {};

declare global {
interface Window {
electronAPI?: {
onBackupData: (fn: (event: any, data?: Record<string, string>) => void) => void;
};
}
}
73 changes: 72 additions & 1 deletion packages/state/src/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { combineReducers } from "@reduxjs/toolkit";
import { type Storage, persistReducer } from "redux-persist";
import {
type PersistConfig,
type PersistedState,
type Storage,
getStoredState,
persistReducer,
} from "redux-persist";
import createWebStorage from "redux-persist/lib/storage/createWebStorage";

import { createAsyncMigrate } from "./createAsyncMigrate";
Expand Down Expand Up @@ -32,21 +38,86 @@ const getTestStorage = () => {
: TEST_STORAGE;
};

const processMigrationData = (backupData: any) => {
try {
const processedData: { accounts: any; root: any } = {
accounts: {},
root: {},
};

if (backupData["persist:accounts"]) {
const accounts = backupData["persist:accounts"];

for (const item in accounts) {
processedData.accounts[item] = JSON.parse(accounts[item]);
}
}

if (backupData["persist:root"]) {
const root = backupData["persist:root"];

for (const item in root) {
processedData.root[item] = JSON.parse(root[item]);
}
}

return processedData;
} catch (error) {
console.error("Error processing backup data:", error);
return null;
}
};

export const makeReducer = (storage_: Storage | undefined) => {
const storage = storage_ || getTestStorage() || createWebStorage("local");

// Custom getStoredState function to handle migration from desktop v2.3.3 to v2.3.4
const customGetStoredState = async (config: PersistConfig<any>): Promise<PersistedState> => {
try {
const state = (await getStoredState(config)) as PersistedState;

const MIGRATION_KEY = "migration_2_3_3_to_2_3_4_completed";
const isMigrationCompleted = localStorage.getItem(MIGRATION_KEY);

if (isMigrationCompleted && state) {
return state;
}

if (window.electronAPI) {
return new Promise(resolve => {
window.electronAPI?.onBackupData((_, data) => {
if (data) {
const processed = processMigrationData(data);

if (processed) {
localStorage.setItem(MIGRATION_KEY, "true");
return resolve(processed[config.key as keyof typeof processed]);
}
}
resolve(undefined);
});
});
}
} catch (err) {
console.error("Error getting stored state:", err);
return;
}
};

const rootPersistConfig = {
key: "root",
version: VERSION,
storage,
blacklist: ["accounts"],
getStoredState: customGetStoredState,
migrate: createAsyncMigrate(mainStoreMigrations, { debug: false }),
};

const accountsPersistConfig = {
key: "accounts",
version: VERSION,
storage,
getStoredState: customGetStoredState,
migrate: createAsyncMigrate(accountsMigrations, { debug: false }),
blacklist: ["password"],
};
Expand Down
Loading
Loading