From 468e60e39a56fce95b89449eb0f77141521520f7 Mon Sep 17 00:00:00 2001 From: James Dunkerley Date: Thu, 4 Jul 2024 15:58:47 +0100 Subject: [PATCH] Auto create the root folder if not present. (#10444) Adds `filesystem-exists` to Project Manager CLI. If the root folder isn't present will add it at start up. (cherry picked from commit 891f176e9c37243fc320a55aac223863c4f6eb0e) --- .../dashboard/src/services/LocalBackend.ts | 111 ++++++++++-------- .../dashboard/src/services/ProjectManager.ts | 14 +++ .../src/projectManagerShimMiddleware.ts | 11 ++ .../org/enso/projectmanager/boot/Cli.scala | 10 ++ .../projectmanager/boot/ProjectManager.scala | 6 + .../filesystem/FileSystemExistsCommand.scala | 51 ++++++++ .../protocol/FileSystemManagementApi.scala | 17 +++ .../projectmanager/protocol/JsonRpc.scala | 1 + .../filesystem/FileSystemService.scala | 8 ++ .../filesystem/FileSystemServiceApi.scala | 7 ++ .../filesystem/FileSystemServiceSpec.scala | 24 ++++ 11 files changed, 209 insertions(+), 51 deletions(-) create mode 100644 lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemExistsCommand.scala diff --git a/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts b/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts index 33608b58d998..9b021618259d 100644 --- a/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts +++ b/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts @@ -124,58 +124,67 @@ export default class LocalBackend extends Backend { ): Promise { const parentIdRaw = query.parentId == null ? null : extractTypeAndId(query.parentId).id const parentId = query.parentId ?? newDirectoryId(this.projectManager.rootDirectory) - const entries = await this.projectManager.listDirectory(parentIdRaw) - return entries - .map(entry => { - switch (entry.type) { - case projectManager.FileSystemEntryType.DirectoryEntry: { - return { - type: backend.AssetType.directory, - id: newDirectoryId(entry.path), - modifiedAt: entry.attributes.lastModifiedTime, - parentId, - title: fileInfo.fileName(entry.path), - permissions: [], - projectState: null, - labels: [], - description: null, - } satisfies backend.DirectoryAsset - } - case projectManager.FileSystemEntryType.ProjectEntry: { - return { - type: backend.AssetType.project, - id: newProjectId(entry.metadata.id), - title: entry.metadata.name, - modifiedAt: entry.metadata.lastOpened ?? entry.metadata.created, - parentId, - permissions: [], - projectState: { - type: - this.projectManager.projects.get(entry.metadata.id)?.state ?? - backend.ProjectState.closed, - volumeId: '', - path: entry.path, - }, - labels: [], - description: null, - } satisfies backend.ProjectAsset - } - case projectManager.FileSystemEntryType.FileEntry: { - return { - type: backend.AssetType.file, - id: newFileId(entry.path), - title: fileInfo.fileName(entry.path), - modifiedAt: entry.attributes.lastModifiedTime, - parentId, - permissions: [], - projectState: null, - labels: [], - description: null, - } satisfies backend.FileAsset + // Check if Root Directory Exists + if ( + parentIdRaw == null && + !(await this.projectManager.exists(this.projectManager.rootDirectory)) + ) { + await this.projectManager.createDirectory(this.projectManager.rootDirectory) + return [] + } else { + const entries = await this.projectManager.listDirectory(parentIdRaw) + return entries + .map(entry => { + switch (entry.type) { + case projectManager.FileSystemEntryType.DirectoryEntry: { + return { + type: backend.AssetType.directory, + id: newDirectoryId(entry.path), + modifiedAt: entry.attributes.lastModifiedTime, + parentId, + title: fileInfo.fileName(entry.path), + permissions: [], + projectState: null, + labels: [], + description: null, + } satisfies backend.DirectoryAsset + } + case projectManager.FileSystemEntryType.ProjectEntry: { + return { + type: backend.AssetType.project, + id: newProjectId(entry.metadata.id), + title: entry.metadata.name, + modifiedAt: entry.metadata.lastOpened ?? entry.metadata.created, + parentId, + permissions: [], + projectState: { + type: + this.projectManager.projects.get(entry.metadata.id)?.state ?? + backend.ProjectState.closed, + volumeId: '', + path: entry.path, + }, + labels: [], + description: null, + } satisfies backend.ProjectAsset + } + case projectManager.FileSystemEntryType.FileEntry: { + return { + type: backend.AssetType.file, + id: newFileId(entry.path), + title: fileInfo.fileName(entry.path), + modifiedAt: entry.attributes.lastModifiedTime, + parentId, + permissions: [], + projectState: null, + labels: [], + description: null, + } satisfies backend.FileAsset + } } - } - }) - .sort(backend.compareAssets) + }) + .sort(backend.compareAssets) + } } /** Return a list of projects belonging to the current user. diff --git a/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts b/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts index e75190df3d9f..8530c781bf4d 100644 --- a/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts +++ b/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts @@ -415,6 +415,20 @@ export default class ProjectManager { return await this.sendRequest('engine/list-available', {}) } + /** Checks if a file or directory exists. */ + async exists(parentId: Path | null) { + /** The type of the response body of this endpoint. */ + interface ResponseBody { + readonly exists: boolean + } + const response = await this.runStandaloneCommand( + null, + 'filesystem-exists', + parentId ?? this.rootDirectory + ) + return response.exists + } + /** List directories, projects and files in the given folder. */ async listDirectory(parentId: Path | null) { /** The type of the response body of this endpoint. */ diff --git a/app/ide-desktop/lib/project-manager-shim/src/projectManagerShimMiddleware.ts b/app/ide-desktop/lib/project-manager-shim/src/projectManagerShimMiddleware.ts index db1442efd8af..9112809135f3 100644 --- a/app/ide-desktop/lib/project-manager-shim/src/projectManagerShimMiddleware.ts +++ b/app/ide-desktop/lib/project-manager-shim/src/projectManagerShimMiddleware.ts @@ -214,6 +214,17 @@ export default function projectManagerShimMiddleware( }) try { switch (cliArguments[0]) { + case '--filesystem-exists': { + const directoryPath = cliArguments[1] + if (directoryPath != null) { + const exists = await fs + .access(directoryPath) + .then(() => true) + .catch(() => false) + result = toJSONRPCResult({ exists }) + } + break + } case '--filesystem-list': { const directoryPath = cliArguments[1] if (directoryPath != null) { diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Cli.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Cli.scala index b354466563cd..a762d6cdbbd6 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Cli.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Cli.scala @@ -17,6 +17,7 @@ object Cli { val PROJECTS_DIRECTORY = "projects-directory" val PROJECT_LIST = "project-list" + val FILESYSTEM_EXISTS = "filesystem-exists" val FILESYSTEM_LIST = "filesystem-list" val FILESYSTEM_CREATE_DIRECTORY = "filesystem-create-directory" val FILESYSTEM_DELETE = "filesystem-delete" @@ -90,6 +91,14 @@ object Cli { .desc("List user projects.") .build() + val filesystemExists: cli.Option = cli.Option.builder + .hasArg(true) + .numberOfArgs(1) + .argName("path") + .longOpt(FILESYSTEM_EXISTS) + .desc("Check if a file or directory exists.") + .build() + val filesystemList: cli.Option = cli.Option.builder .hasArg(true) .numberOfArgs(1) @@ -150,6 +159,7 @@ object Cli { .addOption(option.profilingTime) .addOption(option.projectsDirectory) .addOption(option.projectList) + .addOption(option.filesystemExists) .addOption(option.filesystemList) .addOption(option.filesystemCreateDirectory) .addOption(option.filesystemDelete) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala index f132d2d7a1e7..9019d576bec7 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala @@ -12,6 +12,7 @@ import org.enso.projectmanager.boot.Globals.{ import org.enso.projectmanager.boot.command.filesystem.{ FileSystemCreateDirectoryCommand, FileSystemDeleteCommand, + FileSystemExistsCommand, FileSystemListCommand, FileSystemMoveDirectoryCommand, FileSystemWritePathCommand @@ -218,6 +219,11 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { ZIO.succeed(SuccessExitCode) } else if (options.hasOption(Cli.VERSION_OPTION)) { displayVersion(options.hasOption(Cli.JSON_OPTION)) + } else if (options.hasOption(Cli.FILESYSTEM_EXISTS)) { + val path = Paths.get(options.getOptionValue(Cli.FILESYSTEM_EXISTS)) + val fileSystemExistsCommand = + FileSystemExistsCommand[ZIO[ZAny, +*, +*]](config, path.toFile) + commandHandler.printJson(fileSystemExistsCommand.run) } else if (options.hasOption(Cli.FILESYSTEM_LIST)) { val directory = Paths.get(options.getOptionValue(Cli.FILESYSTEM_LIST)) val fileSystemListCommand = diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemExistsCommand.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemExistsCommand.scala new file mode 100644 index 000000000000..88f50cd1f133 --- /dev/null +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemExistsCommand.scala @@ -0,0 +1,51 @@ +package org.enso.projectmanager.boot.command.filesystem + +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.file.BlockingFileSystem +import org.enso.projectmanager.infrastructure.random.SystemGenerator +import org.enso.projectmanager.infrastructure.repository.ProjectFileRepositoryFactory +import org.enso.projectmanager.infrastructure.time.RealClock +import org.enso.projectmanager.protocol.FileSystemManagementApi.FileSystemExists +import org.enso.projectmanager.service.filesystem.{ + FileSystemService, + FileSystemServiceApi, + FileSystemServiceFailure +} + +import java.io.File + +final class FileSystemExistsCommand[ + F[+_, +_]: CovariantFlatMap +](service: FileSystemServiceApi[F], path: File) { + + def run: F[FileSystemServiceFailure, FileSystemExists.Result] = + service + .exists(path) + .map(FileSystemExists.Result) +} + +object FileSystemExistsCommand { + + def apply[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel: Sync]( + config: ProjectManagerConfig, + path: File + ): FileSystemExistsCommand[F] = { + 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 service = new FileSystemService[F](fileSystem, projectRepositoryFactory) + + new FileSystemExistsCommand[F](service, path) + } + +} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/FileSystemManagementApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/FileSystemManagementApi.scala index 13442804e44f..4e69dae9e1aa 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/FileSystemManagementApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/FileSystemManagementApi.scala @@ -24,6 +24,23 @@ object FileSystemManagementApi { } } + case object FileSystemExists extends Method("filesystem/exists") { + + case class Params(path: File) + + case class Result(exists: Boolean) + + implicit val hasParams: HasParams.Aux[this.type, FileSystemExists.Params] = + new HasParams[this.type] { + type Params = FileSystemExists.Params + } + + implicit val hasResult: HasResult.Aux[this.type, FileSystemExists.Result] = + new HasResult[this.type] { + type Result = FileSystemExists.Result + } + } + case object FileSystemCreateDirectory extends Method("filesystem/createDirectory") { diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala index 51b22889f5c5..19e12f1be37c 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala @@ -38,6 +38,7 @@ object JsonRpc { .registerRequest(ConfigDelete) .registerRequest(LoggingServiceGetEndpoint) .registerRequest(FileSystemList) + .registerRequest(FileSystemExists) .registerRequest(FileSystemCreateDirectory) .registerRequest(FileSystemDeleteDirectory) .registerRequest(FileSystemMoveDirectory) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemService.scala index 3e3cceb9d525..2dd24543d91e 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemService.scala @@ -21,6 +21,14 @@ class FileSystemService[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel]( projectRepositoryFactory: ProjectRepositoryFactory[F] ) extends FileSystemServiceApi[F] { + /** @inheritdoc */ + override def exists(path: File): F[FileSystemServiceFailure, Boolean] = + fileSystem + .exists(path) + .mapError(_ => + FileSystemServiceFailure.FileSystem("Failed to check if path exists") + ) + /** @inheritdoc */ override def list( path: File diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceApi.scala index 039f8d72b2af..679e96162128 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceApi.scala @@ -4,6 +4,13 @@ import java.io.{File, InputStream} trait FileSystemServiceApi[F[+_, +_]] { + /** Checks if the file or directory exists. + * + * @param path the file or directory to check + * @return true if the file or directory exists, false otherwise + */ + def exists(path: File): F[FileSystemServiceFailure, Boolean] + /** List file system entries in the provided directory * * @param path the directory to list diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceSpec.scala index 8c0d67a6a853..4a3c6d6cfaec 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/service/filesystem/FileSystemServiceSpec.scala @@ -204,5 +204,29 @@ class FileSystemServiceSpec FileUtils.deleteQuietly(filePath) } + "check existence of a path" in { + implicit val client: WsTestClient = new WsTestClient(address) + + val testDir = testStorageConfig.userProjectsPath + + val projectName = "New_Project_1" + createProject(projectName) + + val testFile = new File(testDir, "foo.txt") + val dummyFile = new File(testDir, "foo.exe") + + Files.createFile(testFile.toPath) + + val result1 = fileSystemService + .exists(testFile) + .unsafeRunSync() + result1.value should be(true) + + val result2 = fileSystemService + .exists(dummyFile) + .unsafeRunSync() + result2.value should be(false) + } + } }