From 3e9ffb8c578b4d1b497ef4a4bbfd4b4337496559 Mon Sep 17 00:00:00 2001 From: Juan Cazala Date: Wed, 29 Jan 2025 10:30:11 -0300 Subject: [PATCH] fix: config reset on app restart and improve migrations handling (#394) * fix: config reset * fix: add migration that auto imports all scenes --- packages/main/src/modules/analytics.ts | 3 +- packages/main/src/modules/config.ts | 46 ++++++----- packages/main/src/modules/migrations.ts | 93 ++++++++++++++++++++++- packages/preload/src/modules/workspace.ts | 3 +- packages/shared/types/config.ts | 4 +- packages/shared/types/storage.ts | 7 ++ 6 files changed, 130 insertions(+), 26 deletions(-) diff --git a/packages/main/src/modules/analytics.ts b/packages/main/src/modules/analytics.ts index efb24d9..7318aae 100644 --- a/packages/main/src/modules/analytics.ts +++ b/packages/main/src/modules/analytics.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import log from 'electron-log'; import { randomUUID, type UUID } from 'node:crypto'; import { FileSystemStorage } from '/shared/types/storage'; -import { config } from './config'; +import { getConfig } from './config'; import { getWorkspaceConfigPath } from './electron'; let analytics: Analytics | null = null; @@ -19,6 +19,7 @@ export function setUserId(userId: string) { } export async function getAnonymousId() { + const config = await getConfig(); const userId = await config.get('userId'); if (!userId) { const uuid = randomUUID(); diff --git a/packages/main/src/modules/config.ts b/packages/main/src/modules/config.ts index a55c365..126eb41 100644 --- a/packages/main/src/modules/config.ts +++ b/packages/main/src/modules/config.ts @@ -1,30 +1,36 @@ import path from 'node:path'; -import { FileSystemStorage } from '/shared/types/storage'; +import { FileSystemStorage, type IFileSystemStorage } from '/shared/types/storage'; import { SETTINGS_DIRECTORY, CONFIG_FILE_NAME, getFullScenesPath } from '/shared/paths'; -import { DEFAULT_CONFIG, mergeConfig } from '/shared/types/config'; +import { DEFAULT_CONFIG, mergeConfig, type Config } from '/shared/types/config'; import { getUserDataPath } from './electron'; +import { waitForMigrations } from './migrations'; +import log from 'electron-log/main'; export const CONFIG_PATH = path.join(getUserDataPath(), SETTINGS_DIRECTORY, CONFIG_FILE_NAME); -const storage = await FileSystemStorage.getOrCreate(CONFIG_PATH); -// Initialize with default values if empty -const existingConfig = await storage.get>(''); -const defaultConfig = { ...DEFAULT_CONFIG }; -defaultConfig.settings.scenesPath = getFullScenesPath(getUserDataPath()); +let configStorage: IFileSystemStorage | undefined; -if (!existingConfig || Object.keys(existingConfig).length === 0) { - // Write the default config - for (const [key, value] of Object.entries(defaultConfig)) { - await storage.set(key, value); - } -} else { - // Deep merge with defaults if config exists but might be missing properties - const mergedConfig = mergeConfig(existingConfig, defaultConfig); - if (JSON.stringify(existingConfig) !== JSON.stringify(mergedConfig)) { - for (const [key, value] of Object.entries(mergedConfig)) { - await storage.set(key, value); +export async function getConfig(): Promise { + await waitForMigrations(); + + if (!configStorage) { + configStorage = await FileSystemStorage.getOrCreate(CONFIG_PATH); + + // Initialize with default values if empty or merge with defaults if partial + const defaultConfig = { ...DEFAULT_CONFIG }; + defaultConfig.settings.scenesPath = getFullScenesPath(getUserDataPath()); + + const existingConfig = await configStorage.getAll>(); + + // Deep merge with defaults if config exists but might be missing properties + const mergedConfig = mergeConfig(existingConfig, defaultConfig); + if (JSON.stringify(existingConfig) !== JSON.stringify(mergedConfig)) { + log.info('[Config] Writing merged config to storage'); + await configStorage.setAll(mergedConfig); + } else { + log.info('[Config] Config already exists and is up to date'); } } -} -export const config = storage; + return configStorage; +} diff --git a/packages/main/src/modules/migrations.ts b/packages/main/src/modules/migrations.ts index b332014..22f2648 100644 --- a/packages/main/src/modules/migrations.ts +++ b/packages/main/src/modules/migrations.ts @@ -1,14 +1,101 @@ import path from 'path'; import fs from 'fs/promises'; import log from 'electron-log/main'; +import { future } from 'fp-future'; import { SCENES_DIRECTORY } from '/shared/paths'; import { getAppHomeLegacy, getUserDataPath } from './electron'; import { CONFIG_PATH } from './config'; +import { FileSystemStorage } from '/shared/types/storage'; +import { type Config, CURRENT_CONFIG_VERSION } from '/shared/types/config'; + +const migrationsFuture = future(); + +export async function waitForMigrations(): Promise { + return migrationsFuture; +} export async function runMigrations() { - log.info('[Migrations] Starting migrations'); - await migrateLegacyPaths(); - log.info('[Migrations] Migrations completed'); + try { + log.info('[Migrations] Starting migrations'); + await migrateLegacyPaths(); + await migrateToV2(); + log.info('[Migrations] Migrations completed'); + migrationsFuture.resolve(); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + migrationsFuture.reject(err); + throw error; + } +} + +async function migrateToV2() { + log.info('[Migration] Checking if migration to V2 is needed'); + + try { + const storage = await FileSystemStorage.getOrCreate(CONFIG_PATH); + const config = await storage.getAll>(); + + // Only run if version is 1 + if (config.version !== 1) { + log.info('[Migration] Config version is not 1, skipping V2 migration'); + return; + } + + log.info('[Migration] Starting V2 migration'); + + // Check if scenesPath exists + if (config.settings?.scenesPath) { + log.info('[Migration] Scanning scenesPath for scenes'); + + const scenesPath = path.isAbsolute(config.settings.scenesPath) + ? config.settings.scenesPath + : path.join(getUserDataPath(), config.settings.scenesPath); + + try { + // Get all directories in scenesPath + const entries = await fs.readdir(scenesPath, { withFileTypes: true }); + const validScenePaths: string[] = []; + + // Check each directory for valid scenes + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const fullPath = path.join(scenesPath, entry.name); + if (await isValidScene(fullPath)) { + validScenePaths.push(fullPath); + log.info('[Migration] Found valid scene:', fullPath); + } + } + + // Get existing workspace paths or empty array if none exist + const existingPaths = config.workspace?.paths || []; + + // Combine existing paths with new valid scene paths, removing duplicates + const uniquePaths = [...new Set([...existingPaths, ...validScenePaths])]; + + // Ensure workspace object exists + if (!config.workspace) { + config.workspace = { paths: [] }; + } + + // Update workspace paths with combined unique paths + config.workspace.paths = uniquePaths; + + log.info('[Migration] Updated workspace paths:', uniquePaths); + } catch (error) { + log.error('[Migration] Error scanning scenesPath:', error); + throw error; + } + } + + // Update version to 2 + config.version = CURRENT_CONFIG_VERSION; + await storage.setAll(config); + log.info('[Migration] Successfully completed V2 migration'); + } catch (error) { + log.error('[Migration] Error in V2 migration:', error); + throw error; + } } async function isValidScene(scenePath: string): Promise { diff --git a/packages/preload/src/modules/workspace.ts b/packages/preload/src/modules/workspace.ts index e187425..a343949 100644 --- a/packages/preload/src/modules/workspace.ts +++ b/packages/preload/src/modules/workspace.ts @@ -304,9 +304,11 @@ export async function duplicateProject(_path: string): Promise { * @throws An error if the selected directory is not a valid project. */ export async function importProject(): Promise { + const config = await getConfig(); const [projectPath] = await invoke('electron.showOpenDialog', { title: 'Import project', properties: ['openDirectory'], + defaultPath: config.settings.scenesPath, }); const cancelled = !projectPath; @@ -314,7 +316,6 @@ export async function importProject(): Promise { if (cancelled) return undefined; const pathBaseName = path.basename(projectPath); - const config = await getConfig(); const [projects] = await getProjects(config.workspace.paths); const projectAlreadyExists = projects.find($ => $.path === projectPath); diff --git a/packages/shared/types/config.ts b/packages/shared/types/config.ts index 6ca0376..29395a1 100644 --- a/packages/shared/types/config.ts +++ b/packages/shared/types/config.ts @@ -3,6 +3,8 @@ import { type AppSettings } from './settings'; import { DEFAULT_DEPENDENCY_UPDATE_STRATEGY } from './settings'; import { SCENES_DIRECTORY } from '/shared/paths'; +export const CURRENT_CONFIG_VERSION = 2; + export type Config = { version: number; workspace: { @@ -13,7 +15,7 @@ export type Config = { }; export const DEFAULT_CONFIG: Config = { - version: 1, + version: CURRENT_CONFIG_VERSION, workspace: { paths: [], }, diff --git a/packages/shared/types/storage.ts b/packages/shared/types/storage.ts index b21bb77..1681a84 100644 --- a/packages/shared/types/storage.ts +++ b/packages/shared/types/storage.ts @@ -44,11 +44,18 @@ async function _createFileSystemStorage(storagePath: string) { const data = await read(); return data[key] as T | undefined; }, + getAll: async (): Promise => { + const data = await read(); + return data as T; + }, set: async (key: string, value: T): Promise => { const data = await read(); data[key] = value; await write(data); }, + setAll: async (data: T): Promise => { + await write(data); + }, has: async (key: string): Promise => { const data = await read(); return key in data;