mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 17:41:53 +03:00
Soft delete projects by moving them to trash (#10440)
close #10357 Changelog: - add: `LinuxTrashBin` implementing Freedesktop.org trash specification - add: `WindowsTrashBin` calling native platform APIs for Windows - add: `MacTrashBin` calling native platform APIs for MacOS - update: `project/delete` method moves projects to trash and falls back to directory deletion
This commit is contained in:
parent
9e19009158
commit
446834b4f9
@ -2870,8 +2870,11 @@ lazy val `desktop-environment` =
|
||||
.settings(
|
||||
frgaalJavaCompilerSetting,
|
||||
libraryDependencies ++= Seq(
|
||||
"junit" % "junit" % junitVersion % Test,
|
||||
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
|
||||
"org.graalvm.sdk" % "graal-sdk" % graalMavenPackagesVersion % "provided",
|
||||
"commons-io" % "commons-io" % commonsIoVersion,
|
||||
"org.slf4j" % "slf4j-api" % slf4jVersion,
|
||||
"junit" % "junit" % junitVersion % Test,
|
||||
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
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,220 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
/**
|
||||
* The Linux trash implementing the <a
|
||||
* href="https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html">FreeDesktop.org Trash
|
||||
* specification</a>.
|
||||
*
|
||||
* <p>A trash directory contains two subdirectories, named info and files. The files directory
|
||||
* contains the trashed files, and the info directory contains the corresponding trashinfo metadata
|
||||
* for each trashed entry in the files directory.
|
||||
*/
|
||||
final class LinuxTrashBin implements TrashBin {
|
||||
|
||||
private static final String XDG_DATA_HOME = "XDG_DATA_HOME";
|
||||
private static final String PATH_TRASH = "Trash";
|
||||
private static final String PATH_FILES = "files";
|
||||
private static final String PATH_INFO = "info";
|
||||
|
||||
private final LinuxDirectories directories = new LinuxDirectories();
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
var trashDir = detectTrashDirectory();
|
||||
return Files.isDirectory(trashDir.resolve(PATH_FILES))
|
||||
&& Files.isDirectory(trashDir.resolve(PATH_INFO));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToTrash(Path path) {
|
||||
var trashDir = detectTrashDirectory();
|
||||
|
||||
if (Files.exists(path) && isSupported()) {
|
||||
try {
|
||||
var trashInfo = TrashInfo.create(trashDir.resolve(PATH_INFO), path);
|
||||
|
||||
try {
|
||||
Files.move(
|
||||
path,
|
||||
trashDir.resolve(PATH_FILES).resolve(trashInfo.fileName),
|
||||
StandardCopyOption.ATOMIC_MOVE);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
boolean isSuccessful;
|
||||
if (Files.isDirectory(path)) {
|
||||
isSuccessful =
|
||||
moveDirectoryToDirectory(path, trashDir.resolve(PATH_FILES), trashInfo.fileName);
|
||||
} else {
|
||||
isSuccessful =
|
||||
moveFileToDirectory(path, trashDir.resolve(PATH_FILES), trashInfo.fileName);
|
||||
}
|
||||
|
||||
if (!isSuccessful) {
|
||||
FileUtils.deleteQuietly(trashInfo.path.toFile());
|
||||
}
|
||||
|
||||
return isSuccessful;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean moveFileToDirectory(Path from, Path to, String fileName) {
|
||||
var source = from.toFile();
|
||||
var destination = to.resolve(fileName).toFile();
|
||||
|
||||
try {
|
||||
FileUtils.copyFile(source, destination);
|
||||
FileUtils.delete(source);
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
FileUtils.deleteQuietly(destination);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean moveDirectoryToDirectory(Path from, Path to, String fileName) {
|
||||
var source = from.toFile();
|
||||
var destination = to.resolve(fileName).toFile();
|
||||
try {
|
||||
FileUtils.copyDirectory(source, destination);
|
||||
FileUtils.deleteDirectory(source);
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
FileUtils.deleteQuietly(destination);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the path to a home trash directory of the current user.
|
||||
*
|
||||
* <p>The home trash directory should be automatically created for any new user. If the directory
|
||||
* does not exist, it will be created.
|
||||
*
|
||||
* @return the path to the trash directory.
|
||||
*/
|
||||
private Path detectTrashDirectory() {
|
||||
var xdgDataHomeOverride = System.getenv(XDG_DATA_HOME);
|
||||
var xdgDataHome =
|
||||
xdgDataHomeOverride == null
|
||||
? directories.getUserHome().resolve(".local").resolve("share")
|
||||
: Path.of(xdgDataHomeOverride);
|
||||
|
||||
var trashDir = xdgDataHome.resolve(PATH_TRASH);
|
||||
|
||||
try {
|
||||
Files.createDirectories(trashDir.resolve(PATH_FILES));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(trashDir.resolve(PATH_INFO));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
return trashDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* The trashinfo metadata file.
|
||||
*
|
||||
* @param path the path to this trashinfo file.
|
||||
* @param fileName the file name that should be used to store the trashed file.
|
||||
*/
|
||||
private record TrashInfo(Path path, String fileName) {
|
||||
|
||||
private static final int SUFFIX_SIZE = 6;
|
||||
private static final int MAX_ATTEMPTS = Byte.MAX_VALUE;
|
||||
private static final String TRASHINFO_EXTENSION = ".trashinfo";
|
||||
|
||||
/**
|
||||
* Create the .trashinfo file containing the deleted file metadata.
|
||||
*
|
||||
* @param trashInfo the path to the trashinfo directory.
|
||||
* @param toDelete the path to the file that should be deleted.
|
||||
* @return the trashinfo metadata file.
|
||||
* @throws IOException if the file creation was unsuccessful.
|
||||
*/
|
||||
public static TrashInfo create(Path trashInfo, Path toDelete) throws IOException {
|
||||
var builder = new StringBuilder();
|
||||
builder.append("[Trash Info]");
|
||||
builder.append(System.lineSeparator());
|
||||
builder.append("Path=");
|
||||
builder.append(toDelete.toAbsolutePath());
|
||||
builder.append(System.lineSeparator());
|
||||
builder.append("DeletionDate=");
|
||||
builder.append(
|
||||
LocalDateTime.now()
|
||||
.truncatedTo(ChronoUnit.SECONDS)
|
||||
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
builder.append(System.lineSeparator());
|
||||
|
||||
return createTrashInfo(trashInfo, toDelete, builder, "", 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the .trashinfo file containing the deleted file metadata.
|
||||
*
|
||||
* <p>In case of a name clash, when the trash already contains the file with the same name, the
|
||||
* trashinfo file is created with a random suffix to resolve the conflict.
|
||||
*
|
||||
* <p>The file creation is atomic to so that if two processes try trash files with the same
|
||||
* filename this will result in two different trash files.
|
||||
*
|
||||
* @param trashInfo the path to the trashinfo directory.
|
||||
* @param toDelete the path to the file that should be deleted.
|
||||
* @param contents the trashinfo file contents.
|
||||
* @param suffix the trashinfo suffix to resolve the file name conflicts.
|
||||
* @param attempts the number of attempts to resolve the name clash.
|
||||
* @return the trashinfo metadata file.
|
||||
* @throws IOException if the file creation was unsuccessful.
|
||||
*/
|
||||
private static TrashInfo createTrashInfo(
|
||||
Path trashInfo, Path toDelete, CharSequence contents, String suffix, int attempts)
|
||||
throws IOException {
|
||||
if (attempts > MAX_ATTEMPTS) {
|
||||
throw new IOException("Failed to create trashinfo file. Max attempts reached.");
|
||||
}
|
||||
|
||||
try {
|
||||
var fileName = toDelete.getFileName().toString() + suffix;
|
||||
var path =
|
||||
Files.writeString(
|
||||
trashInfo.resolve(fileName + TRASHINFO_EXTENSION),
|
||||
contents,
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE_NEW,
|
||||
StandardOpenOption.WRITE);
|
||||
|
||||
return new TrashInfo(path, fileName);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
return createTrashInfo(
|
||||
trashInfo,
|
||||
toDelete,
|
||||
contents,
|
||||
RandomUtils.alphanumericString(SUFFIX_SIZE),
|
||||
attempts + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.graalvm.nativeimage.UnmanagedMemory;
|
||||
import org.graalvm.nativeimage.c.CContext;
|
||||
import org.graalvm.nativeimage.c.function.CFunction;
|
||||
import org.graalvm.nativeimage.c.type.CCharPointer;
|
||||
import org.graalvm.nativeimage.c.type.CTypeConversion;
|
||||
import org.graalvm.word.Pointer;
|
||||
import org.graalvm.word.WordFactory;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@CContext(MacTrashBin.CoreServices.class)
|
||||
final class MacTrashBin implements TrashBin {
|
||||
|
||||
@CFunction
|
||||
static native int FSPathMakeRefWithOptions(
|
||||
CCharPointer path, int flags, Pointer buffer, Pointer any);
|
||||
|
||||
@CFunction
|
||||
static native int FSMoveObjectToTrashSync(Pointer source, Pointer target, int flags);
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToTrash(Path path) {
|
||||
if (Platform.getOperatingSystem().isMacOs()) {
|
||||
try {
|
||||
return moveToTrashImpl(path);
|
||||
} catch (NullPointerException | LinkageError err) {
|
||||
if (!Boolean.getBoolean("com.oracle.graalvm.isaot")) {
|
||||
var logger = LoggerFactory.getLogger(MacTrashBin.class);
|
||||
logger.warn("Moving to MacOS's Trash Bin is not supported in non-AOT mode.");
|
||||
return false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean moveToTrashImpl(Path path) {
|
||||
Pointer source = UnmanagedMemory.malloc(80);
|
||||
Pointer target = UnmanagedMemory.malloc(80);
|
||||
|
||||
var kFSPathMakeRefDoNotFollowLeafSymlink = 0x01;
|
||||
var kFSFileOperationDefaultOptions = 0x00;
|
||||
try (var cPath = CTypeConversion.toCString(path.toString())) {
|
||||
var r1 =
|
||||
FSPathMakeRefWithOptions(
|
||||
cPath.get(), kFSPathMakeRefDoNotFollowLeafSymlink, source, WordFactory.nullPointer());
|
||||
var r2 = FSMoveObjectToTrashSync(source, target, kFSFileOperationDefaultOptions);
|
||||
return r1 == 0 && r2 == 0;
|
||||
} catch (Throwable error) {
|
||||
return false;
|
||||
} finally {
|
||||
UnmanagedMemory.free(source);
|
||||
UnmanagedMemory.free(target);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CoreServices implements CContext.Directives {
|
||||
@Override
|
||||
public boolean isInConfiguration() {
|
||||
return Platform.getOperatingSystem().isMacOs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHeaderFiles() {
|
||||
return List.of("<CoreServices/CoreServices.h>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLibraries() {
|
||||
return List.of("-framework CoreServices");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,86 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
public final class Platform {
|
||||
/** Identification of the desktop platform. */
|
||||
public enum Platform {
|
||||
LINUX,
|
||||
MACOS,
|
||||
WINDOWS;
|
||||
|
||||
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 static final String OS_LINUX = "linux";
|
||||
private static final String OS_MAC = "mac";
|
||||
private static final String OS_WINDOWS = "windows";
|
||||
|
||||
private static final Platform OPERATING_SYSTEM = detectOperatingSystem();
|
||||
|
||||
/**
|
||||
* @GuardedBy("this")
|
||||
*/
|
||||
private Directories directories;
|
||||
|
||||
/**
|
||||
* @GuardedBy("this")
|
||||
*/
|
||||
private TrashBin trashBin;
|
||||
|
||||
private Platform() {}
|
||||
|
||||
public static String getOsName() {
|
||||
return System.getProperty(OS_NAME);
|
||||
private static Platform detectOperatingSystem() {
|
||||
var osName = System.getProperty(OS_NAME);
|
||||
var lowerOsName = osName.toLowerCase();
|
||||
|
||||
if (lowerOsName.contains(OS_LINUX)) {
|
||||
return LINUX;
|
||||
}
|
||||
|
||||
if (lowerOsName.contains(OS_MAC)) {
|
||||
return MACOS;
|
||||
}
|
||||
|
||||
if (lowerOsName.contains(OS_WINDOWS)) {
|
||||
return WINDOWS;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unknown Operrating System: '" + osName + "'");
|
||||
}
|
||||
|
||||
public static boolean isLinux() {
|
||||
return getOsName().toLowerCase().contains(LINUX);
|
||||
public static Platform getOperatingSystem() {
|
||||
return OPERATING_SYSTEM;
|
||||
}
|
||||
|
||||
public static boolean isMacOs() {
|
||||
return getOsName().toLowerCase().contains(MAC);
|
||||
public boolean isLinux() {
|
||||
return this == LINUX;
|
||||
}
|
||||
|
||||
public static boolean isWindows() {
|
||||
return getOsName().toLowerCase().contains(WINDOWS);
|
||||
public boolean isMacOs() {
|
||||
return this == MACOS;
|
||||
}
|
||||
|
||||
public static Directories getDirectories() {
|
||||
return DirectoriesFactory.getInstance();
|
||||
public boolean isWindows() {
|
||||
return this == WINDOWS;
|
||||
}
|
||||
|
||||
public synchronized Directories getDirectories() {
|
||||
if (directories == null) {
|
||||
directories =
|
||||
switch (Platform.getOperatingSystem()) {
|
||||
case LINUX -> new LinuxDirectories();
|
||||
case MACOS -> new MacOsDirectories();
|
||||
case WINDOWS -> new WindowsDirectories();
|
||||
};
|
||||
}
|
||||
return directories;
|
||||
}
|
||||
|
||||
public synchronized TrashBin getTrashBin() {
|
||||
if (trashBin == null) {
|
||||
trashBin =
|
||||
switch (this) {
|
||||
case LINUX -> new LinuxTrashBin();
|
||||
case MACOS -> new MacTrashBin();
|
||||
case WINDOWS -> new WindowsTrashBin();
|
||||
};
|
||||
}
|
||||
return trashBin;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
final class RandomUtils {
|
||||
|
||||
private static final char[] ALPHANUMERIC =
|
||||
new char[] {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
|
||||
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'J', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
|
||||
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||
};
|
||||
|
||||
private RandomUtils() {}
|
||||
|
||||
/** Delay the {@link Random} instance initialization when building the native image. */
|
||||
private static final class LazyRandom {
|
||||
|
||||
private static Random instance = null;
|
||||
|
||||
private LazyRandom() {}
|
||||
|
||||
public static Random getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Random();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random alphanumeric ASCII string.
|
||||
*
|
||||
* @param size the size of the result string.
|
||||
* @return the random string.
|
||||
*/
|
||||
public static synchronized String alphanumericString(int size) {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("String size should be positive.");
|
||||
}
|
||||
|
||||
var random = LazyRandom.getInstance();
|
||||
var builder = new StringBuilder(size);
|
||||
while (builder.length() < size) {
|
||||
builder.append(ALPHANUMERIC[random.nextInt(ALPHANUMERIC.length)]);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Operations with system trash */
|
||||
public sealed interface TrashBin permits LinuxTrashBin, WindowsTrashBin, MacTrashBin {
|
||||
|
||||
/**
|
||||
* @return {@code true} if the trash functionality is supported on this platform.
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
/**
|
||||
* Move the specified path to the trash bin.
|
||||
*
|
||||
* @param path the file path.
|
||||
* @return {@code true} if the operation was successful.
|
||||
*/
|
||||
boolean moveToTrash(Path path);
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.graalvm.nativeimage.StackValue;
|
||||
import org.graalvm.nativeimage.c.CContext;
|
||||
import org.graalvm.nativeimage.c.constant.CConstant;
|
||||
import org.graalvm.nativeimage.c.function.CFunction;
|
||||
import org.graalvm.nativeimage.c.struct.CField;
|
||||
import org.graalvm.nativeimage.c.struct.CStruct;
|
||||
import org.graalvm.nativeimage.c.type.CCharPointer;
|
||||
import org.graalvm.nativeimage.c.type.CTypeConversion;
|
||||
import org.graalvm.word.PointerBase;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@CContext(WindowsTrashBin.ShellApi.class)
|
||||
final class WindowsTrashBin implements TrashBin {
|
||||
@CConstant
|
||||
public static native int FO_DELETE();
|
||||
|
||||
@CConstant
|
||||
public static native int FOF_ALLOWUNDO();
|
||||
|
||||
@CConstant
|
||||
public static native int FOF_NO_UI();
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToTrash(Path path) {
|
||||
if (Platform.getOperatingSystem().isWindows()) {
|
||||
try {
|
||||
return moveToTrashImpl(path);
|
||||
} catch (NullPointerException | LinkageError err) {
|
||||
if (!Boolean.getBoolean("com.oracle.graalvm.isaot")) {
|
||||
var logger = LoggerFactory.getLogger(MacTrashBin.class);
|
||||
logger.warn("Moving to Windows' Trash Bin is not supported in non-AOT mode.");
|
||||
return false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean moveToTrashImpl(Path path) {
|
||||
try (var cPath = CTypeConversion.toCString(path.toString() + "\0")) {
|
||||
var fileop = StackValue.get(SHFileOperation.class);
|
||||
fileop.wFunc(FO_DELETE());
|
||||
fileop.pFrom(cPath.get());
|
||||
fileop.fFlags((short) (FOF_ALLOWUNDO() | FOF_NO_UI()));
|
||||
var res = SHFileOperationA(fileop);
|
||||
return res == 0;
|
||||
} catch (Throwable error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@CStruct(value = "SHFILEOPSTRUCTA")
|
||||
static interface SHFileOperation extends PointerBase {
|
||||
@CField
|
||||
void pFrom(CCharPointer from);
|
||||
|
||||
@CField
|
||||
void wFunc(int op);
|
||||
|
||||
@CField
|
||||
void fFlags(short flags);
|
||||
}
|
||||
|
||||
@CFunction
|
||||
static native int SHFileOperationA(SHFileOperation fileop);
|
||||
|
||||
public static final class ShellApi implements CContext.Directives {
|
||||
|
||||
@Override
|
||||
public boolean isInConfiguration() {
|
||||
return Platform.getOperatingSystem().isWindows();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHeaderFiles() {
|
||||
return List.of("<windows.h>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLibraries() {
|
||||
return List.of("shell32");
|
||||
}
|
||||
}
|
||||
}
|
@ -3,17 +3,11 @@ 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();
|
||||
}
|
||||
private static final Directories directories = Platform.getOperatingSystem().getDirectories();
|
||||
|
||||
@Test
|
||||
public void getUserHome() {
|
||||
@ -24,10 +18,10 @@ public class DirectoriesTest {
|
||||
@Test
|
||||
public void getDocuments() throws IOException {
|
||||
// getDocuments fails on Windows CI
|
||||
if (!Platform.isWindows()) {
|
||||
var documents = directories.getDocuments();
|
||||
Assert.assertTrue(
|
||||
"User documents is not a directory" + documents, Files.isDirectory(documents));
|
||||
}
|
||||
if (Platform.getOperatingSystem().isWindows()) return;
|
||||
|
||||
var documents = directories.getDocuments();
|
||||
Assert.assertTrue(
|
||||
"User documents is not a directory" + documents, Files.isDirectory(documents));
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,18 @@ import org.junit.Test;
|
||||
|
||||
public class PlatformTest {
|
||||
|
||||
@Test
|
||||
public void getOperatingSystem() {
|
||||
Assert.assertNotNull(Platform.getOperatingSystem());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDirectories() {
|
||||
Assert.assertNotNull(Platform.getDirectories());
|
||||
Assert.assertNotNull(Platform.getOperatingSystem().getDirectories());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTrashBin() {
|
||||
Assert.assertNotNull(Platform.getOperatingSystem().getTrashBin());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class RandomUtilsTest {
|
||||
|
||||
@Test
|
||||
public void alphanumericString() {
|
||||
var size = Short.MAX_VALUE;
|
||||
var str = RandomUtils.alphanumericString(size);
|
||||
Assert.assertEquals(size, str.length());
|
||||
Assert.assertTrue(isAsciiAlphanumeric(str));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alphanumericStringEmpty() {
|
||||
var size = 0;
|
||||
var str = RandomUtils.alphanumericString(size);
|
||||
Assert.assertEquals(size, str.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alphanumericStringNegativeSize() {
|
||||
Assert.assertThrows(IllegalArgumentException.class, () -> RandomUtils.alphanumericString(-1));
|
||||
}
|
||||
|
||||
private static boolean isAsciiAlphanumeric(String str) {
|
||||
boolean result = true;
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
var c = str.charAt(i);
|
||||
result = result && (isAsciiLetter(c) || isAsciiDigit(c));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isAsciiLetter(char c) {
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
|
||||
}
|
||||
|
||||
private static boolean isAsciiDigit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package org.enso.desktopenvironment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
public class TrashBinTest {
|
||||
|
||||
private static final TrashBin TRASH_BIN = Platform.getOperatingSystem().getTrashBin();
|
||||
|
||||
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void isSupported() {
|
||||
if (isEnabled()) {
|
||||
Assert.assertTrue(TRASH_BIN.isSupported());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveToTrashFile() throws IOException {
|
||||
if (isEnabled()) {
|
||||
var path = createTempFile(temporaryFolder);
|
||||
|
||||
Assert.assertTrue(TRASH_BIN.moveToTrash(path));
|
||||
Assert.assertFalse(TRASH_BIN.moveToTrash(path));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveToTrashSameFile() throws IOException {
|
||||
if (isEnabled()) {
|
||||
var path = createTempFile(temporaryFolder);
|
||||
|
||||
Assert.assertTrue(TRASH_BIN.moveToTrash(path));
|
||||
|
||||
Files.writeString(path, "");
|
||||
Assert.assertTrue(TRASH_BIN.moveToTrash(path));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveToTrashDirectory() throws IOException {
|
||||
if (isEnabled()) {
|
||||
var path = createTempDirectory(temporaryFolder);
|
||||
Files.writeString(path.resolve("moveToTrashDirectory"), "");
|
||||
|
||||
Assert.assertTrue(TRASH_BIN.moveToTrash(path));
|
||||
Assert.assertFalse(TRASH_BIN.moveToTrash(path));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveToTrashSameDirectory() throws IOException {
|
||||
if (isEnabled()) {
|
||||
var path = createTempDirectory(temporaryFolder);
|
||||
Files.writeString(path.resolve("moveToTrashSameDirectory"), "");
|
||||
|
||||
Assert.assertTrue(TRASH_BIN.moveToTrash(path));
|
||||
|
||||
Files.createDirectory(path);
|
||||
Files.writeString(path.resolve("moveToTrashSameDirectory"), "");
|
||||
Assert.assertTrue(TRASH_BIN.moveToTrash(path));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the test is enabled.
|
||||
*
|
||||
* <p>macOS and Windows trash bin implementation only works in Native Image.
|
||||
*/
|
||||
private static boolean isEnabled() {
|
||||
return Platform.getOperatingSystem().isLinux();
|
||||
}
|
||||
|
||||
private static Path createTempFile(TemporaryFolder temporaryFolder) throws IOException {
|
||||
return temporaryFolder.newFile().toPath();
|
||||
}
|
||||
|
||||
private static Path createTempDirectory(TemporaryFolder temporaryFolder) throws IOException {
|
||||
return temporaryFolder.newFolder("TrashTest").toPath();
|
||||
}
|
||||
}
|
@ -21,7 +21,12 @@ public final class ProjectsMigration {
|
||||
|
||||
public static void migrate(configuration.StorageConfig storageConfig) {
|
||||
var oldProjectsPath =
|
||||
Platform.getDirectories().getUserHome().resolve("enso").resolve("projects").toFile();
|
||||
Platform.getOperatingSystem()
|
||||
.getDirectories()
|
||||
.getUserHome()
|
||||
.resolve("enso")
|
||||
.resolve("projects")
|
||||
.toFile();
|
||||
if (oldProjectsPath.isDirectory()) {
|
||||
try {
|
||||
File newProjectsPath = storageConfig.userProjectsPath();
|
||||
@ -63,7 +68,7 @@ public final class ProjectsMigration {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Platform.isWindows()) {
|
||||
if (!Platform.getOperatingSystem().isWindows()) {
|
||||
try {
|
||||
logger.info("Setting projects directory permissions '{}'.", newProjectsPath);
|
||||
setProjectsDirectoryPermissions(newProjectsPath);
|
||||
|
@ -11,6 +11,7 @@ import org.enso.projectmanager.boot.configuration.{
|
||||
}
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{Async, ErrorChannel, Exec, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.{DesktopTrash, TrashCan}
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.http.ProjectsEndpoint
|
||||
import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
@ -68,8 +69,16 @@ class MainModule[
|
||||
|
||||
lazy val projectValidator = new ProjectNameValidator[F]()
|
||||
|
||||
lazy val trash: TrashCan[F] = DesktopTrash[F]
|
||||
|
||||
lazy val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](config.storage, clock, fileSystem, gen)
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val distributionConfiguration = DefaultDistributionConfiguration
|
||||
val loggingService = Logging.GlobalLoggingService
|
||||
|
@ -5,6 +5,7 @@ import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.{
|
||||
@ -47,13 +48,16 @@ object ProjectListCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
val projectRepository =
|
||||
projectRepositoryFactory.getProjectRepository(projectsPath)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
@ -35,12 +36,16 @@ object FileSystemCreateDirectoryCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
@ -35,12 +36,16 @@ object FileSystemDeleteCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
@ -36,12 +37,16 @@ object FileSystemExistsCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
@ -38,12 +39,16 @@ object FileSystemListCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
@ -37,12 +38,16 @@ object FileSystemMoveDirectoryCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.DesktopTrash
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory
|
||||
@ -36,12 +37,16 @@ object FileSystemWritePathCommand {
|
||||
val clock = new RealClock[F]
|
||||
val fileSystem = new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
val gen = new SystemGenerator[F]
|
||||
val projectRepositoryFactory = new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
val trash = DesktopTrash[F]
|
||||
|
||||
val projectRepositoryFactory =
|
||||
new ProjectFileRepositoryFactory[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
|
||||
|
||||
|
@ -69,7 +69,9 @@ object configuration {
|
||||
@throws[IOException]
|
||||
def userProjectsPath: File = {
|
||||
val projectsRootDirectory =
|
||||
projectsRoot.getOrElse(Platform.getDirectories.getDocuments.toFile)
|
||||
projectsRoot.getOrElse(
|
||||
Platform.getOperatingSystem.getDirectories.getDocuments.toFile
|
||||
)
|
||||
new File(projectsRootDirectory, projectsDirectory)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package org.enso.projectmanager.infrastructure.desktop
|
||||
|
||||
import org.enso.desktopenvironment.{Platform, TrashBin}
|
||||
import org.enso.projectmanager.control.effect.Sync
|
||||
|
||||
import java.io.File
|
||||
|
||||
class DesktopTrash[F[+_, +_]: Sync](trash: TrashBin) extends TrashCan[F] {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def moveToTrash(path: File): F[Nothing, Boolean] =
|
||||
Sync[F].effect(trash.moveToTrash(path.toPath))
|
||||
}
|
||||
|
||||
object DesktopTrash {
|
||||
|
||||
def apply[F[+_, +_]: Sync]: DesktopTrash[F] = {
|
||||
new DesktopTrash(Platform.getOperatingSystem.getTrashBin)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.enso.projectmanager.infrastructure.desktop
|
||||
|
||||
import java.io.File
|
||||
|
||||
/** An abstraction form the trash can.
|
||||
*
|
||||
* @tparam F a monadic context
|
||||
*/
|
||||
trait TrashCan[F[+_, +_]] {
|
||||
|
||||
/** Moves the provided path to the trash bin.
|
||||
*
|
||||
* @return `true` if the operation was successful
|
||||
*/
|
||||
def moveToTrash(path: File): F[Nothing, Boolean]
|
||||
}
|
@ -15,6 +15,7 @@ import org.enso.projectmanager.control.core.{
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.TrashCan
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystem
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.{
|
||||
FileNotFound,
|
||||
@ -35,6 +36,7 @@ import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
||||
* @param clock a clock
|
||||
* @param fileSystem a file system abstraction
|
||||
* @param gen a random generator
|
||||
* @param trash a trash can
|
||||
*/
|
||||
class ProjectFileRepository[
|
||||
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
|
||||
@ -43,7 +45,8 @@ class ProjectFileRepository[
|
||||
metadataStorageConfig: MetadataStorageConfig,
|
||||
clock: Clock[F],
|
||||
fileSystem: FileSystem[F],
|
||||
gen: Generator[F]
|
||||
gen: Generator[F],
|
||||
trash: TrashCan[F]
|
||||
) extends ProjectRepository[F] {
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -238,6 +241,19 @@ class ProjectFileRepository[
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def moveToTrash(
|
||||
projectId: UUID
|
||||
): F[ProjectRepositoryFailure, Boolean] = {
|
||||
findById(projectId)
|
||||
.flatMap {
|
||||
case Some(project) =>
|
||||
trash.moveToTrash(project.path)
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def moveProject(
|
||||
projectId: UUID,
|
||||
|
@ -2,6 +2,7 @@ package org.enso.projectmanager.infrastructure.repository
|
||||
import org.enso.projectmanager.boot.configuration.StorageConfig
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.desktop.TrashCan
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystem
|
||||
import org.enso.projectmanager.infrastructure.random.Generator
|
||||
import org.enso.projectmanager.infrastructure.time.Clock
|
||||
@ -14,7 +15,8 @@ class ProjectFileRepositoryFactory[
|
||||
storageConfig: StorageConfig,
|
||||
clock: Clock[F],
|
||||
fileSystem: FileSystem[F],
|
||||
gen: Generator[F]
|
||||
gen: Generator[F],
|
||||
trash: TrashCan[F]
|
||||
) extends ProjectRepositoryFactory[F] {
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -28,7 +30,8 @@ class ProjectFileRepositoryFactory[
|
||||
storageConfig.metadata,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,13 @@ trait ProjectRepository[F[+_, +_]] {
|
||||
*/
|
||||
def delete(projectId: UUID): F[ProjectRepositoryFailure, Unit]
|
||||
|
||||
/** Move the provided project to the trash bin.
|
||||
*
|
||||
* @param projectId the project id trash
|
||||
* @return `true` if the operation was successful
|
||||
*/
|
||||
def moveToTrash(projectId: UUID): F[ProjectRepositoryFailure, Boolean]
|
||||
|
||||
/** Renames a project.
|
||||
*
|
||||
* @param projectId the project id to rename
|
||||
|
@ -144,14 +144,26 @@ class ProjectService[
|
||||
override def deleteUserProject(
|
||||
projectId: UUID,
|
||||
projectsDirectory: Option[File]
|
||||
): F[ProjectServiceFailure, Unit] =
|
||||
log.debug("Deleting project [{}].", projectId) *>
|
||||
ensureProjectIsNotRunning(projectId) *>
|
||||
projectRepositoryFactory
|
||||
.getProjectRepository(projectsDirectory)
|
||||
.delete(projectId)
|
||||
.mapError(toServiceFailure) *>
|
||||
log.info("Project deleted [{}].", projectId)
|
||||
): F[ProjectServiceFailure, Unit] = {
|
||||
val projectRepository =
|
||||
projectRepositoryFactory.getProjectRepository(projectsDirectory)
|
||||
val moveProjectToTrash =
|
||||
projectRepository.moveToTrash(projectId).mapError(toServiceFailure)
|
||||
val deleteProject =
|
||||
projectRepository.delete(projectId).mapError(toServiceFailure)
|
||||
|
||||
for {
|
||||
_ <- log.debug("Preparing to delete the project [{}].", projectId)
|
||||
_ <- ensureProjectIsNotRunning(projectId)
|
||||
_ <- CovariantFlatMap[F].ifM(moveProjectToTrash)(
|
||||
ifTrue = log.info("Project moved to trash [{}].", projectId),
|
||||
ifFalse =
|
||||
log.info("Failed to trash the project. Deleting [{}].", projectId) *>
|
||||
deleteProject *>
|
||||
log.info("Project deleted [{}]", projectId)
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
private def ensureProjectIsNotRunning(
|
||||
projectId: UUID
|
||||
|
@ -38,7 +38,7 @@ public class ProjectsMigrationTest {
|
||||
|
||||
@Test
|
||||
public void setProjectDirectoryPermissions() throws IOException {
|
||||
if (!Platform.isWindows()) {
|
||||
if (!Platform.getOperatingSystem().isWindows()) {
|
||||
File projectsDir = tmp.newFolder("projects");
|
||||
createProjectStructure(projectsDir, "Project1");
|
||||
|
||||
|
@ -42,7 +42,11 @@ import org.enso.projectmanager.service.versionmanagement.{
|
||||
RuntimeVersionManagerFactory
|
||||
}
|
||||
import org.enso.projectmanager.service.{ProjectCreationService, ProjectService}
|
||||
import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock}
|
||||
import org.enso.projectmanager.test.{
|
||||
ObservableGenerator,
|
||||
ProgrammableClock,
|
||||
Shredder
|
||||
}
|
||||
import org.enso.runtimeversionmanager.CurrentVersion
|
||||
import org.enso.runtimeversionmanager.components.GraalVMVersion
|
||||
import org.enso.runtimeversionmanager.test.FakeReleases
|
||||
@ -102,6 +106,8 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
|
||||
|
||||
lazy val gen = new ObservableGenerator[ZAny]()
|
||||
|
||||
lazy val trash = new Shredder[ZAny]
|
||||
|
||||
val testProjectsRoot = Files.createTempDirectory(null).toFile
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testProjectsRoot))
|
||||
|
||||
@ -141,7 +147,8 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
|
||||
testStorageConfig,
|
||||
testClock,
|
||||
fileSystem,
|
||||
gen
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
|
||||
lazy val projectNameValidator = new ProjectNameValidator[ZIO[ZAny, *, *]]()
|
||||
|
@ -35,7 +35,8 @@ class FileSystemServiceSpec
|
||||
config.storage,
|
||||
testClock,
|
||||
fileSystem,
|
||||
gen
|
||||
gen,
|
||||
trash
|
||||
)
|
||||
new FileSystemService[ZIO[ZAny, +*, +*]](
|
||||
fileSystem,
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.enso.projectmanager.test
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.projectmanager.infrastructure.desktop.TrashCan
|
||||
import zio.ZIO
|
||||
|
||||
import java.io.File
|
||||
|
||||
class Shredder[R] extends TrashCan[ZIO[R, +*, +*]] {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def moveToTrash(path: File): ZIO[R, Nothing, Boolean] =
|
||||
ZIO
|
||||
.attemptBlocking(FileUtils.forceDelete(path))
|
||||
.fold(
|
||||
failure = _ => false,
|
||||
success = _ => true
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user