diff --git a/src/main/index.js b/src/main/index.js index 5826de4c7..38c44f7bb 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -97,7 +97,7 @@ if (process.platform === "win32") { return spawn("cmd.exe", ["/c", "mkdir", path.join(USERDATA_PATH, "extras")]) }) .then(()=> { - return spawn("cmd.exe", ["/c", "mkdir", path.join(USERDATA_PATH, "workspaces")]) + return spawn("cmd.exe", ["/c", "mkdir", path.join(USERDATA_PATH, "ui/workspaces")]) }) .then(()=> { return spawn("cmd.exe", ["/c", "mkdir", path.join(USERDATA_PATH, "default")]) @@ -120,7 +120,7 @@ if (process.platform === "win32") { } }); } else { - migrationPromise = Promise.resolve(); + migrationPromise = migration.migrate(USERDATA_PATH); // https://github.com/sindresorhus/fix-path // GUI apps on macOS don't inherit the $PATH defined in your dotfiles (.bashrc/.bash_profile/.zshrc/etc) diff --git a/src/main/init/migration.js b/src/main/init/migration.js index 430b2f97d..9095dd540 100644 --- a/src/main/init/migration.js +++ b/src/main/init/migration.js @@ -1,9 +1,57 @@ import { join } from "path"; -import { copy, pathExists as exists, readdir } from "fs-extra"; +import { copy, pathExists as exists } from "fs-extra"; import { exec } from "child_process"; import * as pkg from "../../../package.json"; +import { readdir, symlink, mkdir, readFile } from "fs/promises"; let migrate, uninstallOld; + +/* + When we introduced Ganache 7 for ethereum chains, we moved the workspaces to + Ganache/ui/workspaces. Previous versions of Ganache-UI would crash loading + these workspaces, so we link the legacy workspaces to the new workspaces + directory. This means that the old workspaces are available to both new and + old versions of Ganache-UI, but new workspaces are only available to new + versions. + The intention is to migrate all Ganache-UI data to the /Ganache/ui directory, + + giving the user the option to move (and migrate the chaindata of) legacy + workspaces. + See: https://github.com/trufflesuite/ganache-ui/pull/5151 +*/ +const linkLegacyWorkspaces = async (configRoot) => { + const legacyWorkspacesDirectory = join(configRoot, "workspaces"); + const newWorkspacesDirectory = join(configRoot, "ui/workspaces"); + + if (!await exists(newWorkspacesDirectory)) { + await mkdir(newWorkspacesDirectory, { recursive: true }) + } + + if (await exists(legacyWorkspacesDirectory)) { + const legacyWorkspaces = await readdir(legacyWorkspacesDirectory, { withFileTypes: true }); + const linkingWorkspaces = legacyWorkspaces.map(async legacyWorkspace => { + try { + const fullPath = join(legacyWorkspacesDirectory, legacyWorkspace.name); + + const settings = await readFile(join(fullPath, "Settings")); + const { flavor } = JSON.parse(settings); + if (flavor === "ethereum" || flavor === "filecoin") { + // silently ignore any workspaces that aren't of a supported flavor + const linkPath = join(newWorkspacesDirectory, legacyWorkspace.name); + if (legacyWorkspace.isDirectory() && !await exists(linkPath)) { + return symlink(fullPath, linkPath, "junction"); + } + } + } catch { + // silently ignore any workspaces that fail to link + } + }); + + return Promise.all(linkingWorkspaces); + } +}; + + if (process.platform == "win32") { const APP_DATA = process.env.APPDATA; const COPY_SETTINGS = { @@ -40,7 +88,7 @@ if (process.platform == "win32") { await Promise.all(promises); } - const getOldGanachePath = ()=>{ + const getOldGanachePath = () => { return join(APP_DATA, "/../Local/Packages/Ganache_zh355ej5cj694/LocalCache/Roaming/Ganache"); } @@ -56,13 +104,14 @@ if (process.platform == "win32") { * workspace folder. */ migrate = async (newGanache) => { - if (!APP_DATA) return; - const oldGanache = getOldGanachePath(); - - if (!(await ganacheExists())) return; + if (APP_DATA && await ganacheExists()) { + const oldGanache = getOldGanachePath(); + + const newGanacheVirtualized = join(APP_DATA, `/../Local/Packages/${pkg.build.appx.identityName}_5dg5pnz03psnj/LocalCache/Roaming/Ganache`); + await Promise.all([moveWorkspaces(oldGanache, newGanache), moveGlobalSettings(oldGanache, newGanache), moveWorkspaces(newGanacheVirtualized, newGanache), moveGlobalSettings(newGanacheVirtualized, newGanache)]); + } - const newGanacheVirtualized = join(APP_DATA, `/../Local/Packages/${pkg.build.appx.identityName}_5dg5pnz03psnj/LocalCache/Roaming/Ganache`); - return Promise.all([moveWorkspaces(oldGanache, newGanache), moveGlobalSettings(oldGanache, newGanache), moveWorkspaces(newGanacheVirtualized, newGanache), moveGlobalSettings(newGanacheVirtualized, newGanache)]); + return linkLegacyWorkspaces(newGanache); }; uninstallOld = async () => { @@ -79,10 +128,11 @@ if (process.platform == "win32") { } } else { const noop = () => Promise.resolve(); - migrate = uninstallOld = noop; + migrate = linkLegacyWorkspaces; + uninstallOld = noop; } export default { migrate, uninstallOld -}; +} diff --git a/src/main/types/workspaces/Workspace.js b/src/main/types/workspaces/Workspace.js index 88440a453..10759eb40 100644 --- a/src/main/types/workspaces/Workspace.js +++ b/src/main/types/workspaces/Workspace.js @@ -1,6 +1,5 @@ import path from "path"; import fse from "fs-extra"; - import WorkspaceSettings from "../settings/WorkspaceSettings"; import ContractCache from "../../../integrations/ethereum/main/types/contracts/ContractCache"; @@ -54,7 +53,7 @@ class Workspace { return path.join(configDirectory, `default_${flavor}`); } } else { - return path.join(configDirectory, "workspaces", sanitizedName); + return path.join(configDirectory, "ui/workspaces", sanitizedName); } } @@ -121,8 +120,16 @@ class Workspace { } delete() { + let workspaceDirectory; + if (fse.lstatSync(this.workspaceDirectory).isSymbolicLink()) { + workspaceDirectory = fse.readlinkSync(this.workspaceDirectory); + fse.unlinkSync(this.workspaceDirectory); + } else { + workspaceDirectory = this.workspaceDirectory; + } + try { - fse.removeSync(this.workspaceDirectory); + fse.removeSync(workspaceDirectory); } catch (e) { // TODO: couldn't delete the directory; probably don't have // permissions or some file is open somewhere. we probably @@ -130,6 +137,9 @@ class Workspace { // a message to renderer process, display toast saying there // were issues, etc.). Don't really have time right now for // a solution here + // todo: if unlinking is successful, but removing the + // directory is not, the link will be recreated during the + // migration process next time the app is started. } } diff --git a/src/main/types/workspaces/WorkspaceManager.js b/src/main/types/workspaces/WorkspaceManager.js index 562ea73da..3a579e614 100644 --- a/src/main/types/workspaces/WorkspaceManager.js +++ b/src/main/types/workspaces/WorkspaceManager.js @@ -1,5 +1,6 @@ import path from "path"; import fse from "fs-extra"; +import { readdirSync } from "fs"; import Workspace from "./Workspace"; import WorkspaceSettings from "../settings/WorkspaceSettings"; @@ -9,21 +10,24 @@ class WorkspaceManager { this.workspaces = []; } - enumerateWorkspaces() { - const workspacesDirectory = path.join(this.directory, "workspaces"); + enumerateWorkspaces() { + const workspacesDirectory = path.join(this.directory, "ui/workspaces"); + if (fse.existsSync(workspacesDirectory)) { - this.workspaces = fse.readdirSync(workspacesDirectory).flatMap((file) => { + this.workspaces = readdirSync(workspacesDirectory, { withFileTypes: true }).flatMap((file) => { // if an osx user navigates to the workspaces directory osx will put a // .DS_Store folder there, ignore and delete these. If the file isn't // a directory, also delete it. + if ( - file === ".DS_Store" || - !fse.lstatSync(path.join(workspacesDirectory, file)).isDirectory() + file.name === ".DS_Store" || + !file.isDirectory() + && !file.isSymbolicLink() ) { try { // remove files and folders that aren't allow in the workspaces // directory - fse.removeSync(path.join(workspacesDirectory, file)); + fse.removeSync(path.join(workspacesDirectory, file.name)); } catch { // ignore } @@ -31,8 +35,8 @@ class WorkspaceManager { } let settings = new WorkspaceSettings( - path.join(workspacesDirectory, file), - path.join(workspacesDirectory, file, "chaindata") + path.join(workspacesDirectory, file.name), + path.join(workspacesDirectory, file.name, "chaindata") ); const isQuickstart = settings.get("isDefault"); @@ -46,12 +50,12 @@ class WorkspaceManager { const name = settings.get("name"); const sanitizedName = Workspace.getSanitizedName(name); - if (sanitizedName !== file) { + if (sanitizedName !== file.name) { // apparently the Settings file has a name that is not equal to the directory, // we need to move the directory try { fse.moveSync( - path.join(workspacesDirectory, file), + path.join(workspacesDirectory, file.name), path.join(workspacesDirectory, sanitizedName) ); } catch (e) {