import fs from 'fs'; import fsx from 'fs-extra'; import { difference } from 'lodash'; import NeDB from 'nedb'; import fsPath from 'path'; import type { BaseModel } from '../models'; import * as models from '../models'; import { getModelName } from '../models'; import type { Settings } from '../models/settings'; import { forceWorkspaceScopeToDesign } from '../sync/git/force-workspace-scope-to-design'; import { trackEvent } from './analytics'; import { database as db } from './database'; async function loadDesignerDb( types: string[], designerDataDir: string, ): Promise> { const designerDb = {}; types.forEach(type => { designerDb[type] = []; // initialize each type to empty array }); const promises = types.map( type => new Promise((resolve, reject) => { const filePath = fsPath.join(designerDataDir, `insomnia.${type}.db`); if (!fs.existsSync(filePath)) { console.log(`[db] db file for ${type} not found: ${filePath}`); resolve(); } // Load the data const collection = new NeDB({ autoload: true, filename: filePath, corruptAlertThreshold: 0.9, }); // Find every entry and store in memory collection.find({}, (err, docs: BaseModel[]) => { if (err) { return reject(err); } (designerDb[type] as Record[]).push(...docs); resolve(); }); }), ); await Promise.all(promises); // Return entries, but no longer linked to the database files return designerDb; } type DBType = Record; export interface MigrationOptions { useDesignerSettings: boolean; copyPlugins: boolean; copyWorkspaces: boolean; designerDataDir: string; coreDataDir: string; } export interface MigrationResult { error?: Error; } async function createCoreBackup(modelTypes: string[], coreDataDir: string) { console.log('[db-merge] creating backup'); const backupDir = fsPath.join(coreDataDir, 'core-backup'); await fsx.remove(backupDir); await fsx.ensureDir(backupDir); // Copy db files const filesToCopy = modelTypes.map(modelType => `insomnia.${modelType}.db`); for (const entryName of filesToCopy) { const src = fsPath.join(coreDataDir, entryName); const dest = fsPath.join(backupDir, entryName); await fsx.copy(src, dest); } // Copy dirs const dirsToCopy = ['plugins', 'responses', 'version-control']; await copyDirs(dirsToCopy, coreDataDir, backupDir); console.log(`[db-merge] backup created at ${backupDir}`); return backupDir; } async function migratePlugins(designerDataDir: string, coreDataDir: string) { const designerPluginDir = fsPath.join(designerDataDir, 'plugins'); const corePluginDir = fsPath.join(coreDataDir, 'plugins'); // get list of plugins in Designer const designerPlugins = await readDirs(designerPluginDir); await removeDirs(designerPlugins, corePluginDir); await copyDirs(designerPlugins, designerPluginDir, corePluginDir); // Remove plugin bundle from installed plugins because it's included with the app now const pluginsToDelete = [ 'insomnia-plugin-kong-bundle', 'insomnia-plugin-kong-declarative-config', 'insomnia-plugin-kong-kubernetes-config', 'insomnia-plugin-kong-portal', ]; await removeDirs(pluginsToDelete, corePluginDir); } async function readDirs(srcDir: string) { if (existsAndIsDirectory(srcDir)) { return await fs.promises.readdir(srcDir); } else { return []; } } async function copyDirs(dirs: string[], srcDir: string, destDir: string) { for (const dir of dirs.filter(c => c)) { const src = fsPath.join(srcDir, dir); const dest = fsPath.join(destDir, dir); // If source exists, ensure the destination exists, and copy into it if (existsAndIsDirectory(src)) { await fsx.ensureDir(dest); await fsx.copy(src, dest); } } } async function removeDirs(dirs: string[], srcDir: string) { for (const dir of dirs.filter(c => c)) { const dirToRemove = fsPath.join(srcDir, dir); if (existsAndIsDirectory(dirToRemove)) { await fsx.remove(dirToRemove); } } } export function existsAndIsDirectory(name: string) { return fs.existsSync(name) && fs.statSync(name).isDirectory(); } export default async function migrateFromDesigner({ useDesignerSettings, designerDataDir, coreDataDir, copyPlugins, copyWorkspaces, }: MigrationOptions) { console.log( `[db-merge] starting process for migrating from ${designerDataDir} to ${coreDataDir}`, ); const nonWorkspaceModels = [ models.stats.type, // TODO: investigate further any implications that may invalidate collected stats models.settings.type, ]; // Every model except those to ignore and settings is a "workspace" model const workspaceModels = difference(models.types(), nonWorkspaceModels); const modelTypesToMerge = []; if (useDesignerSettings) { trackEvent('Data', 'Migration', 'Settings'); // @ts-expect-error -- TSCONVERSION modelTypesToMerge.push(models.settings.type); console.log('[db-merge] keeping settings from Insomnia Designer'); } else { console.log('[db-merge] keeping settings from Insomnia Core'); } if (copyWorkspaces) { trackEvent('Data', 'Migration', 'Workspaces'); // @ts-expect-error -- TSCONVERSION modelTypesToMerge.push(...workspaceModels); } let backupDir = ''; try { // Create core backup backupDir = await createCoreBackup(modelTypesToMerge, coreDataDir); // Load designer database const designerDb: DBType = await loadDesignerDb(modelTypesToMerge, designerDataDir); // For each model, batch upsert entries into the Core database for (const modelType of modelTypesToMerge) { const entries = designerDb[modelType]; // Persist some settings from core if (modelType === models.settings.type) { const coreSettings = await models.settings.getOrCreate(); const propertiesToPersist = [ '_id', 'hasPromptedOnboarding', 'hasPromptedToMigrateFromDesigner', ]; propertiesToPersist.forEach(s => { if (coreSettings.hasOwnProperty(s)) { (entries[0] as Settings)[s] = coreSettings[s]; } }); } // For each workspace coming from Designer, mark workspace.scope as 'design' if (modelType === models.workspace.type) { entries.forEach(forceWorkspaceScopeToDesign); } const entryCount = entries.length; console.log( `[db-merge] merging ${entryCount} ${getModelName(modelType, entryCount)} from Designer`, ); await db.batchModifyDocs({ upsert: entries, remove: [], }); } if (copyWorkspaces) { console.log('[db-merge] migrating version control data from designer to core'); await copyDirs(['version-control'], designerDataDir, coreDataDir); console.log('[db-merge] migrating response cache from designer to core'); await copyDirs(['responses'], designerDataDir, coreDataDir); } if (copyPlugins) { console.log('[db-merge] migrating plugins from designer to core'); trackEvent('Data', 'Migration', 'Plugins'); await migratePlugins(designerDataDir, coreDataDir); } console.log('[db-merge] done!'); trackEvent('Data', 'Migration', 'Success'); return {}; } catch (error) { console.log('[db-merge] an error occurred while migrating'); console.error(error); trackEvent('Data', 'Migration', 'Failure'); await restoreCoreBackup(backupDir, coreDataDir); return { error, } as MigrationResult; } } export async function restoreCoreBackup(backupDir: string, coreDataDir: string) { if (!backupDir) { console.log('[db-merge] nothing to restore; no backup was created'); return; } if (!existsAndIsDirectory(backupDir)) { console.log(`[db-merge] nothing to restore: backup directory doesn't exist at ${backupDir}`); return; } console.log('[db-merge] restoring from backup'); await removeDirs(['plugins', 'responses', 'version-control'], coreDataDir); await fsx.copy(backupDir, coreDataDir); console.log('[db-merge] restored from backup'); }