mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 16:01:30 +03:00
Change the default location for Enso projects (#10318)
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
This commit is contained in:
parent
b8a1b0c366
commit
ad5f2c9121
@ -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'
|
||||
@ -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'
|
||||
|
||||
@ -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. */
|
||||
|
@ -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"
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
@ -369,7 +370,12 @@ export function getProjectRoot(subtreePath: string): string | null {
|
||||
|
||||
/** Get the directory that stores Enso projects. */
|
||||
export function getProjectsDirectory(): string {
|
||||
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. */
|
||||
|
@ -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'
|
||||
@ -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 ===
|
||||
|
12
build.sbt
12
build.sbt
@ -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`)
|
||||
@ -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,
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
public class DirectoriesTest {
|
||||
|
||||
private static Directories directories;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
directories = Platform.getDirectories();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserHome() {
|
||||
var userHome = directories.getUserHome();
|
||||
Assert.assertTrue("User home is not a directory: " + userHome, Files.isDirectory(userHome));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDocuments() throws IOException {
|
||||
var documents = directories.getDocuments();
|
||||
Assert.assertTrue(
|
||||
"User documents is not a directory" + documents, Files.isDirectory(documents));
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class PlatformTest {
|
||||
|
||||
@Test
|
||||
public void getDirectories() {
|
||||
Assert.assertNotNull(Platform.getDirectories());
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
package org.enso.projectmanager.event;
|
@ -0,0 +1,118 @@
|
||||
package org.enso.projectmanager.infrastructure.migration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.enso.desktopenvironment.Platform;
|
||||
import org.enso.projectmanager.boot.configuration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class ProjectsMigration {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectsMigration.class);
|
||||
|
||||
private ProjectsMigration() {}
|
||||
|
||||
public static void migrate(configuration.StorageConfig storageConfig) {
|
||||
var oldProjectsPath =
|
||||
Platform.getDirectories().getUserHome().resolve("enso").resolve("projects").toFile();
|
||||
if (oldProjectsPath.isDirectory()) {
|
||||
try {
|
||||
File newProjectsPath = storageConfig.userProjectsPath();
|
||||
migrateProjectsDirectory(oldProjectsPath, newProjectsPath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Migration aborted. Failed to get user documents directory.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void migrateProjectsDirectory(File oldProjectsPath, File newProjectsPath) {
|
||||
if (oldProjectsPath.isDirectory()) {
|
||||
logger.info(
|
||||
"Running projects migration from '{}' to '{}'.", oldProjectsPath, newProjectsPath);
|
||||
if (newProjectsPath.isDirectory()) {
|
||||
try {
|
||||
logger.info(
|
||||
"Both '{}' and '{}' project directories exist. Cleaning up.",
|
||||
oldProjectsPath,
|
||||
newProjectsPath);
|
||||
FileUtils.deleteDirectory(oldProjectsPath);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
logger.error(
|
||||
"Both '{}' and '{}' project directories exist. Failed to clean up.",
|
||||
oldProjectsPath,
|
||||
newProjectsPath,
|
||||
e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
"Moving projects directory from '{}' to '{}'.", oldProjectsPath, newProjectsPath);
|
||||
moveDirectory(oldProjectsPath, newProjectsPath);
|
||||
} catch (IOException ex) {
|
||||
logger.error("Migration aborted. Failed to copy user projects directory.", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Platform.isWindows()) {
|
||||
try {
|
||||
logger.info("Setting projects directory permissions '{}'.", newProjectsPath);
|
||||
setProjectsDirectoryPermissions(newProjectsPath);
|
||||
} catch (IOException ex) {
|
||||
logger.error(
|
||||
"Failed to set permissions on projects directory '{}'.", newProjectsPath, ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Projects migration successful.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the source directory to the destination directory by renaming the source directory. If
|
||||
* renaming is not supported by the file system or the destination directory is located on a
|
||||
* different file system, tries to copy the source directory and then delete the source.
|
||||
*
|
||||
* @param source the source path
|
||||
* @param destination the destination path
|
||||
* @throws IOException if the moving was unsuccessful
|
||||
*/
|
||||
static void moveDirectory(File source, File destination) throws IOException {
|
||||
try {
|
||||
Files.move(source.toPath(), destination.toPath(), StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
FileUtils.copyDirectory(source, destination);
|
||||
} catch (IOException ex) {
|
||||
FileUtils.deleteQuietly(destination);
|
||||
throw ex;
|
||||
}
|
||||
FileUtils.deleteDirectory(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the projects directory permissions to {@code rwx------}.
|
||||
*
|
||||
* @param path the directory path.
|
||||
* @throws IOException if the action is unsuccessful.
|
||||
*/
|
||||
static void setProjectsDirectoryPermissions(File path) throws IOException {
|
||||
Set<PosixFilePermission> permissions = new HashSet<>();
|
||||
permissions.add(PosixFilePermission.OWNER_READ);
|
||||
permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
|
||||
Files.setPosixFilePermissions(path.toPath(), permissions);
|
||||
}
|
||||
}
|
@ -119,15 +119,13 @@ project-manager {
|
||||
}
|
||||
|
||||
storage {
|
||||
projects-root = ${user.home}/enso
|
||||
projects-root = ${?PROJECTS_ROOT}
|
||||
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
|
||||
user-projects-path = ${project-manager.storage.projects-root}/projects
|
||||
tutorials-path = ${project-manager.storage.projects-root}/tutorials
|
||||
tutorials-cache-path = ${project-manager.storage.projects-root}/.tutorials-cache
|
||||
projects-directory = "enso-projects"
|
||||
metadata {
|
||||
project-metadata-directory = ".enso"
|
||||
project-metadata-file-name = "project.json"
|
||||
}
|
||||
}
|
||||
|
||||
timeout {
|
||||
io-timeout = 5 seconds
|
||||
|
@ -21,10 +21,7 @@ import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.log.Slf4jLogging
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.{
|
||||
ProjectFileRepository,
|
||||
ProjectFileRepositoryFactory
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
import org.enso.projectmanager.infrastructure.time.RealClock
|
||||
import org.enso.projectmanager.protocol.{
|
||||
JsonRpcProtocolFactory,
|
||||
@ -74,14 +71,6 @@ class MainModule[
|
||||
lazy val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](config.storage, clock, fileSystem, gen)
|
||||
|
||||
lazy val projectRepository =
|
||||
new ProjectFileRepository[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
|
||||
val distributionConfiguration = DefaultDistributionConfiguration
|
||||
val loggingService = Logging.GlobalLoggingService
|
||||
|
||||
@ -149,7 +138,7 @@ class MainModule[
|
||||
timeoutConfig = config.timeout
|
||||
)
|
||||
|
||||
lazy val projectsEndpoint = new ProjectsEndpoint(projectRepository)
|
||||
lazy val projectsEndpoint = new ProjectsEndpoint(projectRepositoryFactory)
|
||||
lazy val server =
|
||||
new JsonRpcServer(
|
||||
new JsonRpcProtocolFactory,
|
||||
|
@ -21,6 +21,7 @@ import org.enso.projectmanager.boot.configuration.{
|
||||
MainProcessConfig,
|
||||
ProjectManagerConfig
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.migration.ProjectsMigration
|
||||
import org.enso.projectmanager.protocol.JsonRpcProtocolFactory
|
||||
import org.enso.version.VersionDescription
|
||||
import org.slf4j.event.Level
|
||||
@ -53,11 +54,11 @@ object ProjectManager extends ZIOAppDefault with LazyLogging {
|
||||
new JsonRpcProtocolFactory().getProtocol()
|
||||
)
|
||||
|
||||
val computeThreadPool = new ScheduledThreadPoolExecutor(
|
||||
private val computeThreadPool = new ScheduledThreadPoolExecutor(
|
||||
java.lang.Runtime.getRuntime.availableProcessors()
|
||||
)
|
||||
|
||||
val computeExecutionContext: ExecutionContextExecutor =
|
||||
private val computeExecutionContext: ExecutionContextExecutor =
|
||||
ExecutionContext.fromExecutor(
|
||||
computeThreadPool,
|
||||
th => logger.error("An expected error occurred.", th)
|
||||
@ -77,6 +78,7 @@ object ProjectManager extends ZIOAppDefault with LazyLogging {
|
||||
private def mainProcess(
|
||||
processConfig: MainProcessConfig
|
||||
): ZIO[ZAny, IOException, Unit] = {
|
||||
ProjectsMigration.migrate(config.storage)
|
||||
val mainModule =
|
||||
new MainModule[ZIO[ZAny, +*, +*]](
|
||||
config,
|
||||
|
@ -8,7 +8,7 @@ import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.{
|
||||
ProjectFileRepository,
|
||||
ProjectFileRepositoryFactory,
|
||||
ProjectRepository
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.time.RealClock
|
||||
@ -47,17 +47,15 @@ object ProjectListCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val storageConfig = projectsPath.fold(config.storage)(path =>
|
||||
config.storage.copy(userProjectsPath = path)
|
||||
)
|
||||
|
||||
val projectRepository =
|
||||
new ProjectFileRepository[F](
|
||||
storageConfig,
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val projectRepository =
|
||||
projectRepositoryFactory.getProjectRepository(projectsPath)
|
||||
|
||||
new ProjectListCommand[F](projectRepository, limitOpt)
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package org.enso.projectmanager.boot
|
||||
|
||||
import org.enso.desktopenvironment.Platform
|
||||
import org.slf4j.event.Level
|
||||
|
||||
import java.io.File
|
||||
import java.io.{File, IOException}
|
||||
import java.nio.file.Path
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
object configuration {
|
||||
@ -41,21 +43,37 @@ object configuration {
|
||||
*/
|
||||
case class ServerConfig(host: String, port: Int)
|
||||
|
||||
/** A configuration object for properties of project storage.
|
||||
/** A configuration object for metadata storage.
|
||||
*
|
||||
* @param projectsRoot a project root
|
||||
* @param userProjectsPath a user project root
|
||||
* @param projectMetadataDirectory a directory name containing project
|
||||
* metadata
|
||||
* @param projectMetadataDirectory a directory name containing project metadata
|
||||
* @param projectMetadataFileName a name of project metadata file
|
||||
*/
|
||||
case class StorageConfig(
|
||||
projectsRoot: File,
|
||||
userProjectsPath: File,
|
||||
case class MetadataStorageConfig(
|
||||
projectMetadataDirectory: String,
|
||||
projectMetadataFileName: String
|
||||
)
|
||||
|
||||
/** A configuration object for properties of project storage.
|
||||
*
|
||||
* @param projectsRoot overrides user projects root directory
|
||||
* @param projectsDirectory a user projects directory
|
||||
* @param metadata a metadata storage config
|
||||
*/
|
||||
case class StorageConfig(
|
||||
projectsRoot: Option[File],
|
||||
projectsDirectory: String,
|
||||
metadata: MetadataStorageConfig
|
||||
) {
|
||||
|
||||
/** @return a path to the user projects directory. */
|
||||
@throws[IOException]
|
||||
def userProjectsPath: File = {
|
||||
val projectsRootDirectory =
|
||||
projectsRoot.getOrElse(Platform.getDirectories.getDocuments.toFile)
|
||||
new File(projectsRootDirectory, projectsDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
/** A configuration object for timeout properties.
|
||||
*
|
||||
* @param ioTimeout a timeout for IO operations
|
||||
|
@ -15,7 +15,7 @@ import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.Exec
|
||||
import org.enso.projectmanager.infrastructure.repository.{
|
||||
ProjectRepository,
|
||||
ProjectRepositoryFactory,
|
||||
ProjectRepositoryFailure
|
||||
}
|
||||
|
||||
@ -26,10 +26,13 @@ import scala.util.{Failure, Success}
|
||||
|
||||
final class ProjectsEndpoint[
|
||||
F[+_, +_]: Exec: CovariantFlatMap
|
||||
](repo: ProjectRepository[F])
|
||||
](projectRepositoryFactory: ProjectRepositoryFactory[F])
|
||||
extends Endpoint
|
||||
with LazyLogging {
|
||||
|
||||
private val projectRepository =
|
||||
projectRepositoryFactory.getProjectRepository(None)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def route: Route =
|
||||
projectsEndpoint
|
||||
@ -74,7 +77,7 @@ final class ProjectsEndpoint[
|
||||
projectId: UUID
|
||||
): Future[Either[ProjectRepositoryFailure, Option[EnsoProjectArchive]]] =
|
||||
Exec[F].exec {
|
||||
repo
|
||||
projectRepository
|
||||
.findById(projectId)
|
||||
.map(projectOpt =>
|
||||
projectOpt.map(project =>
|
||||
|
@ -1,9 +1,8 @@
|
||||
package org.enso.projectmanager.infrastructure.repository
|
||||
|
||||
import java.io.File
|
||||
|
||||
import io.circe.generic.auto._
|
||||
import org.enso.projectmanager.boot.configuration.StorageConfig
|
||||
import org.enso.projectmanager.boot.configuration.MetadataStorageConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
@ -27,7 +26,7 @@ import shapeless.{Coproduct, Inl, Inr}
|
||||
/** File based implementation of the project metadata storage.
|
||||
*
|
||||
* @param directory a project directory
|
||||
* @param storageConfig a storage config
|
||||
* @param metadataStorageConfig a metadata storage config
|
||||
* @param clock a clock
|
||||
* @param fileSystem a file system abstraction
|
||||
* @param gen a random generator
|
||||
@ -36,7 +35,7 @@ final class MetadataFileStorage[
|
||||
F[+_, +_]: ErrorChannel: CovariantFlatMap
|
||||
](
|
||||
directory: File,
|
||||
storageConfig: StorageConfig,
|
||||
metadataStorageConfig: MetadataStorageConfig,
|
||||
clock: Clock[F],
|
||||
fileSystem: FileSystem[F],
|
||||
gen: Generator[F]
|
||||
@ -89,8 +88,8 @@ final class MetadataFileStorage[
|
||||
new File(
|
||||
project,
|
||||
new File(
|
||||
storageConfig.projectMetadataDirectory,
|
||||
storageConfig.projectMetadataFileName
|
||||
metadataStorageConfig.projectMetadataDirectory,
|
||||
metadataStorageConfig.projectMetadataFileName
|
||||
).toString
|
||||
)
|
||||
}
|
||||
|
@ -4,9 +4,8 @@ import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
import org.enso.projectmanager.boot.configuration.StorageConfig
|
||||
import org.enso.projectmanager.boot.configuration.MetadataStorageConfig
|
||||
import org.enso.projectmanager.control.core.{
|
||||
Applicative,
|
||||
CovariantFlatMap,
|
||||
@ -31,7 +30,7 @@ import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
||||
|
||||
/** File based implementation of the project repository.
|
||||
*
|
||||
* @param storageConfig a storage config
|
||||
* @param metadataStorageConfig a metadata storage config
|
||||
* @param clock a clock
|
||||
* @param fileSystem a file system abstraction
|
||||
* @param gen a random generator
|
||||
@ -39,7 +38,8 @@ import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
||||
class ProjectFileRepository[
|
||||
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
|
||||
](
|
||||
storageConfig: StorageConfig,
|
||||
projectsPath: File,
|
||||
metadataStorageConfig: MetadataStorageConfig,
|
||||
clock: Clock[F],
|
||||
fileSystem: FileSystem[F],
|
||||
gen: Generator[F]
|
||||
@ -60,7 +60,7 @@ class ProjectFileRepository[
|
||||
/** @inheritdoc */
|
||||
override def getAll(): F[ProjectRepositoryFailure, List[Project]] = {
|
||||
fileSystem
|
||||
.list(storageConfig.userProjectsPath)
|
||||
.list(projectsPath)
|
||||
.map(_.filter(_.isDirectory))
|
||||
.recover { case FileNotFound | NotDirectory =>
|
||||
Nil
|
||||
@ -243,7 +243,7 @@ class ProjectFileRepository[
|
||||
|
||||
for {
|
||||
project <- getProject(projectId)
|
||||
primaryPath = new File(storageConfig.userProjectsPath, newName)
|
||||
primaryPath = new File(projectsPath, newName)
|
||||
finalPath <-
|
||||
if (isLocationOk(project.path, primaryPath)) {
|
||||
CovariantFlatMap[F].pure(primaryPath)
|
||||
@ -283,7 +283,7 @@ class ProjectFileRepository[
|
||||
.tailRecM[ProjectRepositoryFailure, Int, File](0) { number =>
|
||||
val path =
|
||||
new File(
|
||||
storageConfig.userProjectsPath,
|
||||
projectsPath,
|
||||
moduleName + genSuffix(number)
|
||||
)
|
||||
fileSystem
|
||||
@ -307,7 +307,7 @@ class ProjectFileRepository[
|
||||
private def metadataStorage(projectPath: File): MetadataFileStorage[F] =
|
||||
new MetadataFileStorage[F](
|
||||
projectPath,
|
||||
storageConfig,
|
||||
metadataStorageConfig,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
|
@ -21,9 +21,14 @@ class ProjectFileRepositoryFactory[
|
||||
override def getProjectRepository(
|
||||
projectsDirectory: Option[File]
|
||||
): ProjectRepository[F] = {
|
||||
val config = projectsDirectory.fold(storageConfig)(dir =>
|
||||
storageConfig.copy(userProjectsPath = dir)
|
||||
val projectsPath =
|
||||
projectsDirectory.getOrElse(storageConfig.userProjectsPath)
|
||||
new ProjectFileRepository[F](
|
||||
projectsPath,
|
||||
storageConfig.metadata,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
new ProjectFileRepository[F](config, clock, fileSystem, gen)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,124 @@
|
||||
package org.enso.projectmanager.infrastructure.migration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.enso.desktopenvironment.Platform;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
public class ProjectsMigrationTest {
|
||||
|
||||
@Rule public TemporaryFolder tmp = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void moveDirectory() throws IOException {
|
||||
File oldProjectsDir = tmp.newFolder("old-projects");
|
||||
File newProjectsDir = new File(tmp.getRoot(), "new-projects");
|
||||
|
||||
File project1 = createProjectStructure(oldProjectsDir, "Project1");
|
||||
File project2 = createProjectStructure(oldProjectsDir, "Project2");
|
||||
|
||||
Assert.assertTrue(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(project1.isDirectory());
|
||||
Assert.assertTrue(project2.isDirectory());
|
||||
Assert.assertFalse(newProjectsDir.isDirectory());
|
||||
|
||||
ProjectsMigration.moveDirectory(oldProjectsDir, newProjectsDir);
|
||||
|
||||
Assert.assertFalse(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProjectsDir.isDirectory());
|
||||
Assert.assertTrue(new File(newProjectsDir, "Project1").isDirectory());
|
||||
Assert.assertTrue(new File(newProjectsDir, "Project2").isDirectory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setProjectDirectoryPermissions() throws IOException {
|
||||
if (!Platform.isWindows()) {
|
||||
File projectsDir = tmp.newFolder("projects");
|
||||
createProjectStructure(projectsDir, "Project1");
|
||||
|
||||
Assert.assertTrue(projectsDir.isDirectory());
|
||||
|
||||
ProjectsMigration.setProjectsDirectoryPermissions(projectsDir);
|
||||
|
||||
var permissions = Files.getPosixFilePermissions(projectsDir.toPath());
|
||||
Assert.assertEquals("rwx------", PosixFilePermissions.toString(permissions));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void migrateProjectsDirectoryIdempotent() throws IOException {
|
||||
File oldProjectsDir = tmp.newFolder("old-projects");
|
||||
File newProjectsDir = new File(tmp.getRoot(), "new-projects");
|
||||
|
||||
File project1 = createProjectStructure(oldProjectsDir, "Project1");
|
||||
File project2 = createProjectStructure(oldProjectsDir, "Project2");
|
||||
|
||||
Assert.assertTrue(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(project1.isDirectory());
|
||||
Assert.assertTrue(project2.isDirectory());
|
||||
Assert.assertFalse(newProjectsDir.isDirectory());
|
||||
|
||||
ProjectsMigration.migrateProjectsDirectory(oldProjectsDir, newProjectsDir);
|
||||
|
||||
File newProject1 = new File(newProjectsDir, "Project1");
|
||||
File newProject2 = new File(newProjectsDir, "Project2");
|
||||
Assert.assertFalse(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProject1.isDirectory());
|
||||
Assert.assertTrue(newProject2.isDirectory());
|
||||
|
||||
ProjectsMigration.migrateProjectsDirectory(oldProjectsDir, newProjectsDir);
|
||||
|
||||
Assert.assertFalse(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProject1.isDirectory());
|
||||
Assert.assertTrue(newProject2.isDirectory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void migrateProjectsDirectoryCleanupWhenBothExist() throws IOException {
|
||||
File oldProjectsDir = tmp.newFolder("old-projects");
|
||||
File newProjectsDir = new File(tmp.getRoot(), "new-projects");
|
||||
|
||||
File project1 = createProjectStructure(newProjectsDir, "Project1");
|
||||
File project2 = createProjectStructure(newProjectsDir, "Project2");
|
||||
|
||||
Assert.assertTrue(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProjectsDir.isDirectory());
|
||||
Assert.assertTrue(project1.isDirectory());
|
||||
Assert.assertTrue(project2.isDirectory());
|
||||
|
||||
ProjectsMigration.migrateProjectsDirectory(oldProjectsDir, newProjectsDir);
|
||||
|
||||
Assert.assertFalse(oldProjectsDir.isDirectory());
|
||||
Assert.assertTrue(newProjectsDir.isDirectory());
|
||||
Assert.assertTrue(new File(newProjectsDir, "Project1").isDirectory());
|
||||
Assert.assertTrue(new File(newProjectsDir, "Project2").isDirectory());
|
||||
}
|
||||
|
||||
private static File createProjectStructure(File tmp, String name) throws IOException {
|
||||
var projectDir = new File(tmp, name);
|
||||
var srcDir = new File(projectDir, "src");
|
||||
var ensoDir = new File(projectDir, ".enso");
|
||||
|
||||
FileUtils.forceMkdir(srcDir);
|
||||
FileUtils.forceMkdir(ensoDir);
|
||||
|
||||
createNewFile(new File(projectDir, "package.yaml"));
|
||||
createNewFile(new File(srcDir, "Main.enso"));
|
||||
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
private static void createNewFile(File file) throws IOException {
|
||||
if (!file.createNewFile()) {
|
||||
throw new IOException("File '" + file + "' already exists.");
|
||||
}
|
||||
}
|
||||
}
|
@ -56,13 +56,12 @@ project-manager {
|
||||
}
|
||||
|
||||
storage {
|
||||
projects-root = ${user.home}/enso
|
||||
projects-root = ${?PROJECTS_ROOT}
|
||||
project-index-path = ${project-manager.storage.projects-root}/.enso/project-index.json
|
||||
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
|
||||
user-projects-path = ${project-manager.storage.projects-root}/projects
|
||||
tutorials-path = ${project-manager.storage.projects-root}/tutorials
|
||||
tutorials-cache-path = ${project-manager.storage.projects-root}/.tutorials-cache
|
||||
projects-directory = "enso-projects"
|
||||
metadata {
|
||||
project-metadata-directory = ".enso"
|
||||
project-metadata-file-name = "project.json"
|
||||
}
|
||||
}
|
||||
|
||||
timeout {
|
||||
|
@ -108,14 +108,16 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
|
||||
val testDistributionRoot = Files.createTempDirectory(null).toFile
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testDistributionRoot))
|
||||
|
||||
val userProjectDir = new File(testProjectsRoot, "projects")
|
||||
|
||||
lazy val testStorageConfig = StorageConfig(
|
||||
projectsRoot = testProjectsRoot,
|
||||
userProjectsPath = userProjectDir,
|
||||
projectsRoot = Some(testProjectsRoot),
|
||||
projectsDirectory = "enso-projects",
|
||||
metadata = MetadataStorageConfig(
|
||||
projectMetadataDirectory = ".enso",
|
||||
projectMetadataFileName = "project.json"
|
||||
)
|
||||
)
|
||||
|
||||
lazy val userProjectDir = testStorageConfig.userProjectsPath
|
||||
|
||||
lazy val bootloaderConfig = config.bootloader
|
||||
|
||||
|
@ -46,7 +46,7 @@ class FileSystemServiceSpec
|
||||
def metadataFileStorage(directory: File) =
|
||||
new MetadataFileStorage[ZIO[ZAny, +*, +*]](
|
||||
directory,
|
||||
config.storage,
|
||||
config.storage.metadata,
|
||||
testClock,
|
||||
fileSystem,
|
||||
gen
|
||||
|
Loading…
Reference in New Issue
Block a user