Skip to content

Commit

Permalink
Change the default location for Enso projects (#10318)
Browse files Browse the repository at this point in the history
close #10240

Changelog:
- add: `desktop-environment` Java module to detect user environment configuration
- add: `ProjectsMigration` module containing the migration logic of the enso projects directory
- update: updated and cleaned up unused settings from the storage config
- add: `desktopEnvironment` TS module to detect user environment configuration in the `project-manager-shim`
- update: `project-manager-shim` with the new user projects directory
  • Loading branch information
4e6 authored Jun 22, 2024
1 parent b8a1b0c commit ad5f2c9
Show file tree
Hide file tree
Showing 29 changed files with 669 additions and 82 deletions.
4 changes: 2 additions & 2 deletions app/ide-desktop/lib/client/src/bin/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import * as fs from 'node:fs/promises'
import * as fsSync from 'node:fs'
import * as http from 'node:http'
import * as os from 'node:os'
import * as path from 'node:path'
import * as stream from 'node:stream'
import * as mkcert from 'mkcert'
Expand All @@ -17,6 +16,7 @@ import * as common from 'enso-common'
import GLOBAL_CONFIG from 'enso-common/src/config.json' assert { type: 'json' }
import * as contentConfig from 'enso-content-config'
import * as ydocServer from 'enso-gui2/ydoc-server'
import * as projectManagement from 'enso-project-manager-shim/src/projectManagement'

import * as paths from '../paths'

Expand Down Expand Up @@ -92,7 +92,7 @@ export class Server {

/** Create a simple HTTP server. */
constructor(public config: Config) {
this.projectsRootDirectory = path.join(os.homedir(), 'enso/projects')
this.projectsRootDirectory = projectManagement.getProjectsDirectory()
}

/** Server constructor. */
Expand Down
3 changes: 2 additions & 1 deletion app/ide-desktop/lib/project-manager-shim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/projectManagerShimMiddleware.ts"
".": "./src/projectManagerShimMiddleware.ts",
"./src/projectManagement": "./src/projectManagement.ts"
},
"dependencies": {
"yaml": "^2.4.1"
Expand Down
81 changes: 81 additions & 0 deletions app/ide-desktop/lib/project-manager-shim/src/desktopEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @file This module contains the logic for the detection of user-specific desktop environment attributes.
*/

import * as childProcess from 'node:child_process'
import * as os from 'node:os'
import * as path from 'node:path'

export const DOCUMENTS = getDocumentsPath()

const CHILD_PROCESS_TIMEOUT = 3000

/**
* Detects path of the user documents directory depending on the operating system.
*/
function getDocumentsPath(): string | undefined {
if (process.platform === 'linux') {
return getLinuxDocumentsPath()
} else if (process.platform === 'darwin') {
return getMacOsDocumentsPath()
} else if (process.platform === 'win32') {
return getWindowsDocumentsPath()
} else {
return
}
}

/**
* Returns the user documents path on Linux.
*/
function getLinuxDocumentsPath(): string {
const xdgDocumentsPath = getXdgDocumentsPath()

return xdgDocumentsPath ?? path.join(os.homedir(), 'enso')
}

/**
* Gets the documents directory from the XDG directory management system.
*/
function getXdgDocumentsPath(): string | undefined {
const out = childProcess.spawnSync('xdg-user-dir', ['DOCUMENTS'], {
timeout: CHILD_PROCESS_TIMEOUT,
})

if (out.error !== undefined) {
return
} else {
return out.stdout.toString().trim()
}
}

/**
* Get the user documents path. On macOS, `Documents` acts as a symlink pointing to the
* real locale-specific user documents directory.
*/
function getMacOsDocumentsPath(): string {
return path.join(os.homedir(), 'Documents')
}

/**
* Get the path to the `My Documents` Windows directory.
*/
function getWindowsDocumentsPath(): string | undefined {
const out = childProcess.spawnSync(
'reg',
[
'query',
'"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ShellFolders"',
'/v',
'personal',
],
{ timeout: CHILD_PROCESS_TIMEOUT }
)

if (out.error !== undefined) {
return
} else {
const stdoutString = out.stdout.toString()
return stdoutString.split('\\s\\s+')[4]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as tar from 'tar'

import * as common from 'enso-common'
import * as buildUtils from 'enso-common/src/buildUtils'
import * as desktopEnvironment from './desktopEnvironment'

const logger = console

Expand Down Expand Up @@ -369,7 +370,12 @@ export function getProjectRoot(subtreePath: string): string | null {

/** Get the directory that stores Enso projects. */
export function getProjectsDirectory(): string {
return pathModule.join(os.homedir(), 'enso', 'projects')
const documentsPath = desktopEnvironment.DOCUMENTS
if (documentsPath === undefined) {
return pathModule.join(os.homedir(), 'enso', 'projects')
} else {
return pathModule.join(documentsPath, 'enso-projects')
}
}

/** Check if the given project is installed, i.e. can be opened with the Project Manager. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import * as fs from 'node:fs/promises'
import * as fsSync from 'node:fs'
import * as http from 'node:http'
import * as os from 'node:os'
import * as path from 'node:path'

import * as isHiddenFile from 'is-hidden-file'
Expand All @@ -22,7 +21,7 @@ import * as projectManagement from './projectManagement'
const HTTP_STATUS_OK = 200
const HTTP_STATUS_BAD_REQUEST = 400
const HTTP_STATUS_NOT_FOUND = 404
const PROJECTS_ROOT_DIRECTORY = path.join(os.homedir(), 'enso/projects')
const PROJECTS_ROOT_DIRECTORY = projectManagement.getProjectsDirectory()

// =============
// === Types ===
Expand Down
12 changes: 12 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
.value
)
.dependsOn(`akka-native`)
.dependsOn(`desktop-environment`)
.dependsOn(`version-output`)
.dependsOn(editions)
.dependsOn(`edition-updater`)
Expand Down Expand Up @@ -2797,6 +2798,17 @@ lazy val `benchmarks-common` =
)
.dependsOn(`polyglot-api`)

lazy val `desktop-environment` =
project
.in(file("lib/java/desktop-environment"))
.settings(
frgaalJavaCompilerSetting,
libraryDependencies ++= Seq(
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
)
)

lazy val `bench-processor` = (project in file("lib/scala/bench-processor"))
.settings(
frgaalJavaCompilerSetting,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.enso.desktopenvironment;

import java.io.IOException;
import java.nio.file.Path;

/** Provides information about user directories. */
public sealed interface Directories permits LinuxDirectories, MacOsDirectories, WindowsDirectories {

/**
* @return the user home directory.
*/
default Path getUserHome() {
return Path.of(System.getProperty("user.home"));
}

/**
* @return the user documents directory.
* @throws IOException when cannot detect the documents directory of the user.
*/
Path getDocuments() throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.enso.desktopenvironment;

final class DirectoriesFactory {

private static final Directories INSTANCE = initDirectories();

private static Directories initDirectories() {
if (Platform.isLinux()) {
return new LinuxDirectories();
}

if (Platform.isMacOs()) {
return new MacOsDirectories();
}

if (Platform.isWindows()) {
return new WindowsDirectories();
}

throw new UnsupportedOperationException("Unsupported OS '" + Platform.getOsName() + "'");
}

private DirectoriesFactory() {}

public static Directories getInstance() {
return INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.enso.desktopenvironment;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

final class LinuxDirectories implements Directories {

private static final String[] PROCESS_XDG_DOCUMENTS = new String[] {"xdg-user-dir", "DOCUMENTS"};

/**
* Get the user 'Documents' directory.
*
* <p>Tries to obtain the documents directory from the XDG directory management system if
* available and falls back to {@code $HOME/enso}.
*
* @return the path to the user documents directory.
*/
@Override
public Path getDocuments() {
try {
return getXdgDocuments();
} catch (IOException | InterruptedException e) {
return getUserHome().resolve("enso");
}
}

private Path getXdgDocuments() throws IOException, InterruptedException {
var process = new ProcessBuilder(PROCESS_XDG_DOCUMENTS).start();
process.waitFor(3, TimeUnit.SECONDS);

var documentsString =
new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);

return Path.of(documentsString.trim());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.enso.desktopenvironment;

import java.io.IOException;
import java.nio.file.Path;

final class MacOsDirectories implements Directories {

private static final String DOCUMENTS = "Documents";

/**
* Get the user documents path.
*
* <p>On macOS, the 'Documents' directory acts like a symlink and points to the real
* locale-dependent user documents folder.
*
* @return the path to the user documents directory.
* @throws IOException when unable to resolve the real documents path.
*/
@Override
public Path getDocuments() throws IOException {
try {
return getUserHome().resolve(DOCUMENTS).toRealPath();
} catch (IOException e) {
throw new IOException("Failed to resolve real MacOs documents path", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.enso.desktopenvironment;

public final class Platform {

private static final String OS_NAME = "os.name";
private static final String LINUX = "linux";
private static final String MAC = "mac";
private static final String WINDOWS = "windows";

private Platform() {}

public static String getOsName() {
return System.getProperty(OS_NAME);
}

public static boolean isLinux() {
return getOsName().toLowerCase().contains(LINUX);
}

public static boolean isMacOs() {
return getOsName().toLowerCase().contains(MAC);
}

public static boolean isWindows() {
return getOsName().toLowerCase().contains(WINDOWS);
}

public static Directories getDirectories() {
return DirectoriesFactory.getInstance();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.enso.desktopenvironment;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

final class WindowsDirectories implements Directories {

private static final String[] PROCESS_REG_QUERY =
new String[] {
"reg",
"query",
"\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ShellFolders\"",
"/v",
"personal"
};

/**
* Get the path to 'My Documents' user directory.
*
* <p>Method uses the registry query that may not work on Windows XP versions and below.
*
* @return the 'My Documents' user directory path.
* @throws IOException when fails to detect the user documents directory.
*/
@Override
public Path getDocuments() throws IOException {
try {
var process = new ProcessBuilder(PROCESS_REG_QUERY).start();
process.waitFor(3, TimeUnit.SECONDS);

var stdoutString =
new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
var stdoutParts = stdoutString.split("\\s\\s+");
if (stdoutParts.length < 5) {
throw new IOException("Invalid Windows registry query output: '" + stdoutString + "'");
}

return Path.of(stdoutParts[4].trim());
} catch (IOException e) {
throw new IOException("Failed to run Windows registry query", e);
} catch (InterruptedException e) {
throw new IOException("Windows registry query timeout", e);
}
}
}
Loading

0 comments on commit ad5f2c9

Please sign in to comment.