diff --git a/docs/language-server/protocol-project-manager.md b/docs/language-server/protocol-project-manager.md index 489a1cfc339f..dbd6809a630b 100644 --- a/docs/language-server/protocol-project-manager.md +++ b/docs/language-server/protocol-project-manager.md @@ -32,6 +32,7 @@ transport formats, please look [here](./protocol-architecture.md). - [Create Directory](#create-directory) - [Delete Directory](#delete-directory) - [Move File Or Directory](#move-file-or-directory) + - [Write to File](#write-to-file) - [Project Management Operations](#project-management-operations) - [`project/open`](#projectopen) - [`project/close`](#projectclose) @@ -335,6 +336,27 @@ null; #### Errors +- [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with + underlying data store. + +### Write to File + +Writes bytes from stdin to the provided path. + +#### Parameters + +```typescript +echo 'Hello World!' | project-manager --filesystem-write-path {path} +``` + +### Result + +```typescript +null; +``` + +#### Errors + - [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with underlying data store. 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 5a91ceaa5651..2c513b980991 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 @@ -22,6 +22,7 @@ object Cli { val FILESYSTEM_DELETE = "filesystem-delete" val FILESYSTEM_MOVE_FROM = "filesystem-move-from" val FILESYSTEM_MOVE_TO = "filesystem-move-to" + val FILESYSTEM_WRITE_PATH = "filesystem-write-path" object option { @@ -128,6 +129,14 @@ object Cli { .longOpt(FILESYSTEM_MOVE_TO) .desc("Move directory. Destination.") .build() + + val filesystemWritePath: cli.Option = cli.Option.builder + .hasArg(true) + .numberOfArgs(1) + .argName("path") + .longOpt(FILESYSTEM_WRITE_PATH) + .desc("Write data from stdin to the provided file") + .build() } val options: cli.Options = @@ -146,6 +155,7 @@ object Cli { .addOption(option.filesystemDelete) .addOption(option.filesystemMoveFrom) .addOption(option.filesystemMoveTo) + .addOption(option.filesystemWritePath) /** Parse the command line options. */ def parse(args: Array[String]): Either[String, cli.CommandLine] = { 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 9d65f8d7acac..79a1be76ee9f 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 @@ -13,7 +13,8 @@ import org.enso.projectmanager.boot.command.filesystem.{ FileSystemCreateDirectoryCommand, FileSystemDeleteCommand, FileSystemListCommand, - FileSystemMoveDirectoryCommand + FileSystemMoveDirectoryCommand, + FileSystemWritePathCommand } import org.enso.projectmanager.boot.command.{CommandHandler, ProjectListCommand} import org.enso.projectmanager.boot.configuration.{ @@ -245,6 +246,14 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { to.toFile ) commandHandler.printJson(fileSystemMoveDirectoryCommand.run) + } else if (options.hasOption(Cli.FILESYSTEM_WRITE_PATH)) { + val path = Paths.get(options.getOptionValue(Cli.FILESYSTEM_WRITE_PATH)) + val fileSystemMoveDirectoryCommand = + FileSystemWritePathCommand[ZIO[ZAny, +*, +*]]( + config, + path.toFile + ) + commandHandler.printJson(fileSystemMoveDirectoryCommand.run) } else if (options.hasOption(Cli.PROJECT_LIST)) { val projectsPathOpt = Option(options.getOptionValue(Cli.PROJECTS_DIRECTORY)) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemWritePathCommand.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemWritePathCommand.scala new file mode 100644 index 000000000000..766ade014963 --- /dev/null +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/command/filesystem/FileSystemWritePathCommand.scala @@ -0,0 +1,50 @@ +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.FileSystemWritePath +import org.enso.projectmanager.service.filesystem.{ + FileSystemService, + FileSystemServiceApi, + FileSystemServiceFailure +} + +import java.io.{File, InputStream} + +final class FileSystemWritePathCommand[F[+_, +_]: CovariantFlatMap]( + service: FileSystemServiceApi[F], + path: File, + data: InputStream +) { + + def run: F[FileSystemServiceFailure, FileSystemWritePath.Result] = + service.write(path, data).map(_ => FileSystemWritePath.Result) +} + +object FileSystemWritePathCommand { + + def apply[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel: Sync]( + config: ProjectManagerConfig, + path: File + ): FileSystemWritePathCommand[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 FileSystemWritePathCommand[F](service, path, System.in) + } +} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/BlockingFileSystem.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/BlockingFileSystem.scala index d02d21d0b32c..a543909ce637 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/BlockingFileSystem.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/BlockingFileSystem.scala @@ -1,5 +1,5 @@ package org.enso.projectmanager.infrastructure.file -import java.io.{File, FileNotFoundException} +import java.io.{File, FileNotFoundException, InputStream} import java.nio.file.{ AccessDeniedException, NoSuchFileException, @@ -33,6 +33,18 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel]( .mapError(toFsFailure) .timeoutFail(OperationTimeout)(ioTimeout) + /** Writes binary content to a file. + * + * @param file path to the file + * @param contents a textual contents of the file + * @return either [[FileSystemFailure]] or Unit + */ + def writeFile(file: File, contents: InputStream): F[FileSystemFailure, Unit] = + Sync[F] + .blockingOp { FileUtils.copyInputStreamToFile(contents, file) } + .mapError(toFsFailure) + .timeoutFail(OperationTimeout)(ioTimeout) + /** Writes textual content to a file. * * @param file path to the file diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/FileSystem.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/FileSystem.scala index ef9a7b4b3bd8..c10e7772ecac 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/FileSystem.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/file/FileSystem.scala @@ -1,6 +1,6 @@ package org.enso.projectmanager.infrastructure.file -import java.io.File +import java.io.{File, InputStream} /** Represents abstraction for filesystem operations. * @@ -15,6 +15,14 @@ trait FileSystem[F[+_, +_]] { */ def readFile(file: File): F[FileSystemFailure, String] + /** Writes binary content to a file. + * + * @param file path to the file + * @param contents a contents of the file + * @return either [[FileSystemFailure]] or Unit + */ + def writeFile(file: File, contents: InputStream): F[FileSystemFailure, Unit] + /** Writes textual content to a file. * * @param file path to the file 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 940b45a16301..13442804e44f 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 @@ -83,4 +83,23 @@ object FileSystemManagementApi { type Result = Unused.type } } + + case object FileSystemWritePath extends Method("filesystem/writePath") { + + case class Params(path: File) + + type Result = Unused.type + val Result = Unused + + implicit val hasParams + : HasParams.Aux[this.type, FileSystemWritePath.Params] = + new HasParams[this.type] { + type Params = FileSystemWritePath.Params + } + + implicit val hasResult: HasResult.Aux[this.type, Unused.type] = + new HasResult[this.type] { + type Result = Unused.type + } + } } 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 67a27c0ec342..51b22889f5c5 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 @@ -41,6 +41,7 @@ object JsonRpc { .registerRequest(FileSystemCreateDirectory) .registerRequest(FileSystemDeleteDirectory) .registerRequest(FileSystemMoveDirectory) + .registerRequest(FileSystemWritePath) .finalized() } 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 173c94153c39..3e3cceb9d525 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 @@ -12,7 +12,7 @@ import org.enso.projectmanager.control.effect.syntax._ import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFactory import org.enso.projectmanager.service.ProjectService -import java.io.File +import java.io.{File, InputStream} import java.nio.file.Files import java.nio.file.attribute.BasicFileAttributes @@ -56,6 +56,17 @@ class FileSystemService[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel]( .move(from, to) .mapError(_ => FileSystemServiceFailure.FileSystem("Failed to move path")) + /** @inheritdoc */ + override def write( + path: File, + contents: InputStream + ): F[FileSystemServiceFailure, Unit] = + fileSystem + .writeFile(path, contents) + .mapError(_ => + FileSystemServiceFailure.FileSystem("Failed to write path") + ) + private def toFileSystemEntry( path: File ): F[FileSystemServiceFailure, Option[FileSystemEntry]] = { 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 c5826c4b2b76..039f8d72b2af 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 @@ -1,6 +1,6 @@ package org.enso.projectmanager.service.filesystem -import java.io.File +import java.io.{File, InputStream} trait FileSystemServiceApi[F[+_, +_]] { @@ -29,4 +29,11 @@ trait FileSystemServiceApi[F[+_, +_]] { * @param to the destination path */ def move(from: File, to: File): F[FileSystemServiceFailure, Unit] + + /** Writes a file + * + * @param path the file path to write + * @param bytes the file contents + */ + def write(path: File, in: InputStream): F[FileSystemServiceFailure, Unit] } 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 925bef4475e5..281a9ee5b1e5 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 @@ -13,7 +13,8 @@ import org.enso.semver.SemVer import org.scalatest.EitherValues import zio.{ZAny, ZIO} -import java.io.File +import java.io.{ByteArrayInputStream, File} +import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.attribute.BasicFileAttributes @@ -182,5 +183,26 @@ class FileSystemServiceSpec FileUtils.deleteQuietly(targetPath) } + "write path" in { + val testDir = testStorageConfig.userProjectsPath + + val fileName = "filesystem_test_write_path.txt" + val filePath = new File(testDir, fileName) + val contents = "Hello World!" + + fileSystemService + .write( + filePath, + new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)) + ) + .unsafeRunSync() + + val bytes = Files.readAllBytes(filePath.toPath) + new String(bytes, StandardCharsets.UTF_8) shouldEqual contents + + // cleanup + FileUtils.deleteQuietly(filePath) + } + } }