mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 21:01:37 +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 fs from 'node:fs/promises'
|
||||||
import * as fsSync from 'node:fs'
|
import * as fsSync from 'node:fs'
|
||||||
import * as http from 'node:http'
|
import * as http from 'node:http'
|
||||||
import * as os from 'node:os'
|
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import * as stream from 'node:stream'
|
import * as stream from 'node:stream'
|
||||||
import * as mkcert from 'mkcert'
|
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 GLOBAL_CONFIG from 'enso-common/src/config.json' assert { type: 'json' }
|
||||||
import * as contentConfig from 'enso-content-config'
|
import * as contentConfig from 'enso-content-config'
|
||||||
import * as ydocServer from 'enso-gui2/ydoc-server'
|
import * as ydocServer from 'enso-gui2/ydoc-server'
|
||||||
|
import * as projectManagement from 'enso-project-manager-shim/src/projectManagement'
|
||||||
|
|
||||||
import * as paths from '../paths'
|
import * as paths from '../paths'
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export class Server {
|
|||||||
|
|
||||||
/** Create a simple HTTP server. */
|
/** Create a simple HTTP server. */
|
||||||
constructor(public config: Config) {
|
constructor(public config: Config) {
|
||||||
this.projectsRootDirectory = path.join(os.homedir(), 'enso/projects')
|
this.projectsRootDirectory = projectManagement.getProjectsDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Server constructor. */
|
/** Server constructor. */
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/projectManagerShimMiddleware.ts"
|
".": "./src/projectManagerShimMiddleware.ts",
|
||||||
|
"./src/projectManagement": "./src/projectManagement.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yaml": "^2.4.1"
|
"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 common from 'enso-common'
|
||||||
import * as buildUtils from 'enso-common/src/buildUtils'
|
import * as buildUtils from 'enso-common/src/buildUtils'
|
||||||
|
import * as desktopEnvironment from './desktopEnvironment'
|
||||||
|
|
||||||
const logger = console
|
const logger = console
|
||||||
|
|
||||||
@ -369,7 +370,12 @@ export function getProjectRoot(subtreePath: string): string | null {
|
|||||||
|
|
||||||
/** Get the directory that stores Enso projects. */
|
/** Get the directory that stores Enso projects. */
|
||||||
export function getProjectsDirectory(): string {
|
export function getProjectsDirectory(): string {
|
||||||
|
const documentsPath = desktopEnvironment.DOCUMENTS
|
||||||
|
if (documentsPath === undefined) {
|
||||||
return pathModule.join(os.homedir(), 'enso', 'projects')
|
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. */
|
/** 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 fs from 'node:fs/promises'
|
||||||
import * as fsSync from 'node:fs'
|
import * as fsSync from 'node:fs'
|
||||||
import * as http from 'node:http'
|
import * as http from 'node:http'
|
||||||
import * as os from 'node:os'
|
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
|
||||||
import * as isHiddenFile from 'is-hidden-file'
|
import * as isHiddenFile from 'is-hidden-file'
|
||||||
@ -22,7 +21,7 @@ import * as projectManagement from './projectManagement'
|
|||||||
const HTTP_STATUS_OK = 200
|
const HTTP_STATUS_OK = 200
|
||||||
const HTTP_STATUS_BAD_REQUEST = 400
|
const HTTP_STATUS_BAD_REQUEST = 400
|
||||||
const HTTP_STATUS_NOT_FOUND = 404
|
const HTTP_STATUS_NOT_FOUND = 404
|
||||||
const PROJECTS_ROOT_DIRECTORY = path.join(os.homedir(), 'enso/projects')
|
const PROJECTS_ROOT_DIRECTORY = projectManagement.getProjectsDirectory()
|
||||||
|
|
||||||
// =============
|
// =============
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
12
build.sbt
12
build.sbt
@ -1120,6 +1120,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
|
|||||||
.value
|
.value
|
||||||
)
|
)
|
||||||
.dependsOn(`akka-native`)
|
.dependsOn(`akka-native`)
|
||||||
|
.dependsOn(`desktop-environment`)
|
||||||
.dependsOn(`version-output`)
|
.dependsOn(`version-output`)
|
||||||
.dependsOn(editions)
|
.dependsOn(editions)
|
||||||
.dependsOn(`edition-updater`)
|
.dependsOn(`edition-updater`)
|
||||||
@ -2797,6 +2798,17 @@ lazy val `benchmarks-common` =
|
|||||||
)
|
)
|
||||||
.dependsOn(`polyglot-api`)
|
.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"))
|
lazy val `bench-processor` = (project in file("lib/scala/bench-processor"))
|
||||||
.settings(
|
.settings(
|
||||||
frgaalJavaCompilerSetting,
|
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 {
|
storage {
|
||||||
projects-root = ${user.home}/enso
|
|
||||||
projects-root = ${?PROJECTS_ROOT}
|
projects-root = ${?PROJECTS_ROOT}
|
||||||
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
|
projects-directory = "enso-projects"
|
||||||
user-projects-path = ${project-manager.storage.projects-root}/projects
|
metadata {
|
||||||
tutorials-path = ${project-manager.storage.projects-root}/tutorials
|
|
||||||
tutorials-cache-path = ${project-manager.storage.projects-root}/.tutorials-cache
|
|
||||||
project-metadata-directory = ".enso"
|
project-metadata-directory = ".enso"
|
||||||
project-metadata-file-name = "project.json"
|
project-metadata-file-name = "project.json"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timeout {
|
timeout {
|
||||||
io-timeout = 5 seconds
|
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.log.Slf4jLogging
|
||||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||||
import org.enso.projectmanager.infrastructure.repository.{
|
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||||
ProjectFileRepository,
|
|
||||||
ProjectFileRepositoryFactory
|
|
||||||
}
|
|
||||||
import org.enso.projectmanager.infrastructure.time.RealClock
|
import org.enso.projectmanager.infrastructure.time.RealClock
|
||||||
import org.enso.projectmanager.protocol.{
|
import org.enso.projectmanager.protocol.{
|
||||||
JsonRpcProtocolFactory,
|
JsonRpcProtocolFactory,
|
||||||
@ -74,14 +71,6 @@ class MainModule[
|
|||||||
lazy val projectRepositoryFactory =
|
lazy val projectRepositoryFactory =
|
||||||
new ProjectFileRepositoryFactory[F](config.storage, clock, fileSystem, gen)
|
new ProjectFileRepositoryFactory[F](config.storage, clock, fileSystem, gen)
|
||||||
|
|
||||||
lazy val projectRepository =
|
|
||||||
new ProjectFileRepository[F](
|
|
||||||
config.storage,
|
|
||||||
clock,
|
|
||||||
fileSystem,
|
|
||||||
gen
|
|
||||||
)
|
|
||||||
|
|
||||||
val distributionConfiguration = DefaultDistributionConfiguration
|
val distributionConfiguration = DefaultDistributionConfiguration
|
||||||
val loggingService = Logging.GlobalLoggingService
|
val loggingService = Logging.GlobalLoggingService
|
||||||
|
|
||||||
@ -149,7 +138,7 @@ class MainModule[
|
|||||||
timeoutConfig = config.timeout
|
timeoutConfig = config.timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val projectsEndpoint = new ProjectsEndpoint(projectRepository)
|
lazy val projectsEndpoint = new ProjectsEndpoint(projectRepositoryFactory)
|
||||||
lazy val server =
|
lazy val server =
|
||||||
new JsonRpcServer(
|
new JsonRpcServer(
|
||||||
new JsonRpcProtocolFactory,
|
new JsonRpcProtocolFactory,
|
||||||
|
@ -21,6 +21,7 @@ import org.enso.projectmanager.boot.configuration.{
|
|||||||
MainProcessConfig,
|
MainProcessConfig,
|
||||||
ProjectManagerConfig
|
ProjectManagerConfig
|
||||||
}
|
}
|
||||||
|
import org.enso.projectmanager.infrastructure.migration.ProjectsMigration
|
||||||
import org.enso.projectmanager.protocol.JsonRpcProtocolFactory
|
import org.enso.projectmanager.protocol.JsonRpcProtocolFactory
|
||||||
import org.enso.version.VersionDescription
|
import org.enso.version.VersionDescription
|
||||||
import org.slf4j.event.Level
|
import org.slf4j.event.Level
|
||||||
@ -53,11 +54,11 @@ object ProjectManager extends ZIOAppDefault with LazyLogging {
|
|||||||
new JsonRpcProtocolFactory().getProtocol()
|
new JsonRpcProtocolFactory().getProtocol()
|
||||||
)
|
)
|
||||||
|
|
||||||
val computeThreadPool = new ScheduledThreadPoolExecutor(
|
private val computeThreadPool = new ScheduledThreadPoolExecutor(
|
||||||
java.lang.Runtime.getRuntime.availableProcessors()
|
java.lang.Runtime.getRuntime.availableProcessors()
|
||||||
)
|
)
|
||||||
|
|
||||||
val computeExecutionContext: ExecutionContextExecutor =
|
private val computeExecutionContext: ExecutionContextExecutor =
|
||||||
ExecutionContext.fromExecutor(
|
ExecutionContext.fromExecutor(
|
||||||
computeThreadPool,
|
computeThreadPool,
|
||||||
th => logger.error("An expected error occurred.", th)
|
th => logger.error("An expected error occurred.", th)
|
||||||
@ -77,6 +78,7 @@ object ProjectManager extends ZIOAppDefault with LazyLogging {
|
|||||||
private def mainProcess(
|
private def mainProcess(
|
||||||
processConfig: MainProcessConfig
|
processConfig: MainProcessConfig
|
||||||
): ZIO[ZAny, IOException, Unit] = {
|
): ZIO[ZAny, IOException, Unit] = {
|
||||||
|
ProjectsMigration.migrate(config.storage)
|
||||||
val mainModule =
|
val mainModule =
|
||||||
new MainModule[ZIO[ZAny, +*, +*]](
|
new MainModule[ZIO[ZAny, +*, +*]](
|
||||||
config,
|
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.file.BlockingFileSystem
|
||||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||||
import org.enso.projectmanager.infrastructure.repository.{
|
import org.enso.projectmanager.infrastructure.repository.{
|
||||||
ProjectFileRepository,
|
ProjectFileRepositoryFactory,
|
||||||
ProjectRepository
|
ProjectRepository
|
||||||
}
|
}
|
||||||
import org.enso.projectmanager.infrastructure.time.RealClock
|
import org.enso.projectmanager.infrastructure.time.RealClock
|
||||||
@ -47,17 +47,15 @@ object ProjectListCommand {
|
|||||||
val clock = new RealClock[F]
|
val clock = new RealClock[F]
|
||||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||||
val gen = new SystemGenerator[F]
|
val gen = new SystemGenerator[F]
|
||||||
val storageConfig = projectsPath.fold(config.storage)(path =>
|
|
||||||
config.storage.copy(userProjectsPath = path)
|
|
||||||
)
|
|
||||||
|
|
||||||
val projectRepository =
|
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||||
new ProjectFileRepository[F](
|
config.storage,
|
||||||
storageConfig,
|
|
||||||
clock,
|
clock,
|
||||||
fileSystem,
|
fileSystem,
|
||||||
gen
|
gen
|
||||||
)
|
)
|
||||||
|
val projectRepository =
|
||||||
|
projectRepositoryFactory.getProjectRepository(projectsPath)
|
||||||
|
|
||||||
new ProjectListCommand[F](projectRepository, limitOpt)
|
new ProjectListCommand[F](projectRepository, limitOpt)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package org.enso.projectmanager.boot
|
package org.enso.projectmanager.boot
|
||||||
|
|
||||||
|
import org.enso.desktopenvironment.Platform
|
||||||
import org.slf4j.event.Level
|
import org.slf4j.event.Level
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{File, IOException}
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
import scala.concurrent.duration.FiniteDuration
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
|
||||||
object configuration {
|
object configuration {
|
||||||
@ -41,21 +43,37 @@ object configuration {
|
|||||||
*/
|
*/
|
||||||
case class ServerConfig(host: String, port: Int)
|
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 projectMetadataDirectory a directory name containing project metadata
|
||||||
* @param userProjectsPath a user project root
|
|
||||||
* @param projectMetadataDirectory a directory name containing project
|
|
||||||
* metadata
|
|
||||||
* @param projectMetadataFileName a name of project metadata file
|
* @param projectMetadataFileName a name of project metadata file
|
||||||
*/
|
*/
|
||||||
case class StorageConfig(
|
case class MetadataStorageConfig(
|
||||||
projectsRoot: File,
|
|
||||||
userProjectsPath: File,
|
|
||||||
projectMetadataDirectory: String,
|
projectMetadataDirectory: String,
|
||||||
projectMetadataFileName: 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.
|
/** A configuration object for timeout properties.
|
||||||
*
|
*
|
||||||
* @param ioTimeout a timeout for IO operations
|
* @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.core.syntax._
|
||||||
import org.enso.projectmanager.control.effect.Exec
|
import org.enso.projectmanager.control.effect.Exec
|
||||||
import org.enso.projectmanager.infrastructure.repository.{
|
import org.enso.projectmanager.infrastructure.repository.{
|
||||||
ProjectRepository,
|
ProjectRepositoryFactory,
|
||||||
ProjectRepositoryFailure
|
ProjectRepositoryFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,10 +26,13 @@ import scala.util.{Failure, Success}
|
|||||||
|
|
||||||
final class ProjectsEndpoint[
|
final class ProjectsEndpoint[
|
||||||
F[+_, +_]: Exec: CovariantFlatMap
|
F[+_, +_]: Exec: CovariantFlatMap
|
||||||
](repo: ProjectRepository[F])
|
](projectRepositoryFactory: ProjectRepositoryFactory[F])
|
||||||
extends Endpoint
|
extends Endpoint
|
||||||
with LazyLogging {
|
with LazyLogging {
|
||||||
|
|
||||||
|
private val projectRepository =
|
||||||
|
projectRepositoryFactory.getProjectRepository(None)
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
override def route: Route =
|
override def route: Route =
|
||||||
projectsEndpoint
|
projectsEndpoint
|
||||||
@ -74,7 +77,7 @@ final class ProjectsEndpoint[
|
|||||||
projectId: UUID
|
projectId: UUID
|
||||||
): Future[Either[ProjectRepositoryFailure, Option[EnsoProjectArchive]]] =
|
): Future[Either[ProjectRepositoryFailure, Option[EnsoProjectArchive]]] =
|
||||||
Exec[F].exec {
|
Exec[F].exec {
|
||||||
repo
|
projectRepository
|
||||||
.findById(projectId)
|
.findById(projectId)
|
||||||
.map(projectOpt =>
|
.map(projectOpt =>
|
||||||
projectOpt.map(project =>
|
projectOpt.map(project =>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package org.enso.projectmanager.infrastructure.repository
|
package org.enso.projectmanager.infrastructure.repository
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
import io.circe.generic.auto._
|
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.CovariantFlatMap
|
||||||
import org.enso.projectmanager.control.core.syntax._
|
import org.enso.projectmanager.control.core.syntax._
|
||||||
import org.enso.projectmanager.control.effect.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.
|
/** File based implementation of the project metadata storage.
|
||||||
*
|
*
|
||||||
* @param directory a project directory
|
* @param directory a project directory
|
||||||
* @param storageConfig a storage config
|
* @param metadataStorageConfig a metadata storage config
|
||||||
* @param clock a clock
|
* @param clock a clock
|
||||||
* @param fileSystem a file system abstraction
|
* @param fileSystem a file system abstraction
|
||||||
* @param gen a random generator
|
* @param gen a random generator
|
||||||
@ -36,7 +35,7 @@ final class MetadataFileStorage[
|
|||||||
F[+_, +_]: ErrorChannel: CovariantFlatMap
|
F[+_, +_]: ErrorChannel: CovariantFlatMap
|
||||||
](
|
](
|
||||||
directory: File,
|
directory: File,
|
||||||
storageConfig: StorageConfig,
|
metadataStorageConfig: MetadataStorageConfig,
|
||||||
clock: Clock[F],
|
clock: Clock[F],
|
||||||
fileSystem: FileSystem[F],
|
fileSystem: FileSystem[F],
|
||||||
gen: Generator[F]
|
gen: Generator[F]
|
||||||
@ -89,8 +88,8 @@ final class MetadataFileStorage[
|
|||||||
new File(
|
new File(
|
||||||
project,
|
project,
|
||||||
new File(
|
new File(
|
||||||
storageConfig.projectMetadataDirectory,
|
metadataStorageConfig.projectMetadataDirectory,
|
||||||
storageConfig.projectMetadataFileName
|
metadataStorageConfig.projectMetadataFileName
|
||||||
).toString
|
).toString
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,8 @@ import java.io.File
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.attribute.FileTime
|
import java.nio.file.attribute.FileTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
import org.enso.pkg.{Package, PackageManager}
|
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.{
|
import org.enso.projectmanager.control.core.{
|
||||||
Applicative,
|
Applicative,
|
||||||
CovariantFlatMap,
|
CovariantFlatMap,
|
||||||
@ -31,7 +30,7 @@ import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
|||||||
|
|
||||||
/** File based implementation of the project repository.
|
/** File based implementation of the project repository.
|
||||||
*
|
*
|
||||||
* @param storageConfig a storage config
|
* @param metadataStorageConfig a metadata storage config
|
||||||
* @param clock a clock
|
* @param clock a clock
|
||||||
* @param fileSystem a file system abstraction
|
* @param fileSystem a file system abstraction
|
||||||
* @param gen a random generator
|
* @param gen a random generator
|
||||||
@ -39,7 +38,8 @@ import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
|||||||
class ProjectFileRepository[
|
class ProjectFileRepository[
|
||||||
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
|
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
|
||||||
](
|
](
|
||||||
storageConfig: StorageConfig,
|
projectsPath: File,
|
||||||
|
metadataStorageConfig: MetadataStorageConfig,
|
||||||
clock: Clock[F],
|
clock: Clock[F],
|
||||||
fileSystem: FileSystem[F],
|
fileSystem: FileSystem[F],
|
||||||
gen: Generator[F]
|
gen: Generator[F]
|
||||||
@ -60,7 +60,7 @@ class ProjectFileRepository[
|
|||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
override def getAll(): F[ProjectRepositoryFailure, List[Project]] = {
|
override def getAll(): F[ProjectRepositoryFailure, List[Project]] = {
|
||||||
fileSystem
|
fileSystem
|
||||||
.list(storageConfig.userProjectsPath)
|
.list(projectsPath)
|
||||||
.map(_.filter(_.isDirectory))
|
.map(_.filter(_.isDirectory))
|
||||||
.recover { case FileNotFound | NotDirectory =>
|
.recover { case FileNotFound | NotDirectory =>
|
||||||
Nil
|
Nil
|
||||||
@ -243,7 +243,7 @@ class ProjectFileRepository[
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
project <- getProject(projectId)
|
project <- getProject(projectId)
|
||||||
primaryPath = new File(storageConfig.userProjectsPath, newName)
|
primaryPath = new File(projectsPath, newName)
|
||||||
finalPath <-
|
finalPath <-
|
||||||
if (isLocationOk(project.path, primaryPath)) {
|
if (isLocationOk(project.path, primaryPath)) {
|
||||||
CovariantFlatMap[F].pure(primaryPath)
|
CovariantFlatMap[F].pure(primaryPath)
|
||||||
@ -283,7 +283,7 @@ class ProjectFileRepository[
|
|||||||
.tailRecM[ProjectRepositoryFailure, Int, File](0) { number =>
|
.tailRecM[ProjectRepositoryFailure, Int, File](0) { number =>
|
||||||
val path =
|
val path =
|
||||||
new File(
|
new File(
|
||||||
storageConfig.userProjectsPath,
|
projectsPath,
|
||||||
moduleName + genSuffix(number)
|
moduleName + genSuffix(number)
|
||||||
)
|
)
|
||||||
fileSystem
|
fileSystem
|
||||||
@ -307,7 +307,7 @@ class ProjectFileRepository[
|
|||||||
private def metadataStorage(projectPath: File): MetadataFileStorage[F] =
|
private def metadataStorage(projectPath: File): MetadataFileStorage[F] =
|
||||||
new MetadataFileStorage[F](
|
new MetadataFileStorage[F](
|
||||||
projectPath,
|
projectPath,
|
||||||
storageConfig,
|
metadataStorageConfig,
|
||||||
clock,
|
clock,
|
||||||
fileSystem,
|
fileSystem,
|
||||||
gen
|
gen
|
||||||
|
@ -21,9 +21,14 @@ class ProjectFileRepositoryFactory[
|
|||||||
override def getProjectRepository(
|
override def getProjectRepository(
|
||||||
projectsDirectory: Option[File]
|
projectsDirectory: Option[File]
|
||||||
): ProjectRepository[F] = {
|
): ProjectRepository[F] = {
|
||||||
val config = projectsDirectory.fold(storageConfig)(dir =>
|
val projectsPath =
|
||||||
storageConfig.copy(userProjectsPath = dir)
|
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 {
|
storage {
|
||||||
projects-root = ${user.home}/enso
|
|
||||||
projects-root = ${?PROJECTS_ROOT}
|
projects-root = ${?PROJECTS_ROOT}
|
||||||
project-index-path = ${project-manager.storage.projects-root}/.enso/project-index.json
|
projects-directory = "enso-projects"
|
||||||
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
|
metadata {
|
||||||
user-projects-path = ${project-manager.storage.projects-root}/projects
|
project-metadata-directory = ".enso"
|
||||||
tutorials-path = ${project-manager.storage.projects-root}/tutorials
|
project-metadata-file-name = "project.json"
|
||||||
tutorials-cache-path = ${project-manager.storage.projects-root}/.tutorials-cache
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout {
|
timeout {
|
||||||
|
@ -108,14 +108,16 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
|
|||||||
val testDistributionRoot = Files.createTempDirectory(null).toFile
|
val testDistributionRoot = Files.createTempDirectory(null).toFile
|
||||||
sys.addShutdownHook(FileUtils.deleteQuietly(testDistributionRoot))
|
sys.addShutdownHook(FileUtils.deleteQuietly(testDistributionRoot))
|
||||||
|
|
||||||
val userProjectDir = new File(testProjectsRoot, "projects")
|
|
||||||
|
|
||||||
lazy val testStorageConfig = StorageConfig(
|
lazy val testStorageConfig = StorageConfig(
|
||||||
projectsRoot = testProjectsRoot,
|
projectsRoot = Some(testProjectsRoot),
|
||||||
userProjectsPath = userProjectDir,
|
projectsDirectory = "enso-projects",
|
||||||
|
metadata = MetadataStorageConfig(
|
||||||
projectMetadataDirectory = ".enso",
|
projectMetadataDirectory = ".enso",
|
||||||
projectMetadataFileName = "project.json"
|
projectMetadataFileName = "project.json"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val userProjectDir = testStorageConfig.userProjectsPath
|
||||||
|
|
||||||
lazy val bootloaderConfig = config.bootloader
|
lazy val bootloaderConfig = config.bootloader
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class FileSystemServiceSpec
|
|||||||
def metadataFileStorage(directory: File) =
|
def metadataFileStorage(directory: File) =
|
||||||
new MetadataFileStorage[ZIO[ZAny, +*, +*]](
|
new MetadataFileStorage[ZIO[ZAny, +*, +*]](
|
||||||
directory,
|
directory,
|
||||||
config.storage,
|
config.storage.metadata,
|
||||||
testClock,
|
testClock,
|
||||||
fileSystem,
|
fileSystem,
|
||||||
gen
|
gen
|
||||||
|
Loading…
Reference in New Issue
Block a user