Skip to content

Commit

Permalink
fix: config reset on app restart and improve migrations handling (#394)
Browse files Browse the repository at this point in the history
* fix: config reset

* fix: add migration that auto imports all scenes
  • Loading branch information
cazala authored Jan 29, 2025
1 parent a8780c9 commit 3e9ffb8
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 26 deletions.
3 changes: 2 additions & 1 deletion packages/main/src/modules/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +19,7 @@ export function setUserId(userId: string) {
}

export async function getAnonymousId() {
const config = await getConfig();
const userId = await config.get<string>('userId');
if (!userId) {
const uuid = randomUUID();
Expand Down
46 changes: 26 additions & 20 deletions packages/main/src/modules/config.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>>('');
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<IFileSystemStorage> {
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<Partial<Config>>();

// 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;
}
93 changes: 90 additions & 3 deletions packages/main/src/modules/migrations.ts
Original file line number Diff line number Diff line change
@@ -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<void>();

export async function waitForMigrations(): Promise<void> {
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<Partial<Config>>();

// 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<boolean> {
Expand Down
3 changes: 2 additions & 1 deletion packages/preload/src/modules/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,17 +304,18 @@ export async function duplicateProject(_path: string): Promise<Project> {
* @throws An error if the selected directory is not a valid project.
*/
export async function importProject(): Promise<Project | undefined> {
const config = await getConfig();
const [projectPath] = await invoke('electron.showOpenDialog', {
title: 'Import project',
properties: ['openDirectory'],
defaultPath: config.settings.scenesPath,
});

const cancelled = !projectPath;

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);

Expand Down
4 changes: 3 additions & 1 deletion packages/shared/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -13,7 +15,7 @@ export type Config = {
};

export const DEFAULT_CONFIG: Config = {
version: 1,
version: CURRENT_CONFIG_VERSION,
workspace: {
paths: [],
},
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/types/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ async function _createFileSystemStorage(storagePath: string) {
const data = await read();
return data[key] as T | undefined;
},
getAll: async <T extends StorageData>(): Promise<T> => {
const data = await read();
return data as T;
},
set: async <T>(key: string, value: T): Promise<void> => {
const data = await read();
data[key] = value;
await write(data);
},
setAll: async <T extends StorageData>(data: T): Promise<void> => {
await write(data);
},
has: async (key: string): Promise<boolean> => {
const data = await read();
return key in data;
Expand Down

0 comments on commit 3e9ffb8

Please sign in to comment.