Allow project manager to read files (#11204)

close #11187

Changelog:
- add: `--filesystem-read-path` project manager command to read a file path and return its contents to stdout
This commit is contained in:
Dmitry Bushev 2024-10-01 19:52:29 +03:00 committed by GitHub
parent 7f9cf7a916
commit 0b8a0c493a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 201 additions and 17 deletions

View File

@ -32,6 +32,7 @@ transport formats, please look [here](./protocol-architecture.md).
- [Create Directory](#create-directory) - [Create Directory](#create-directory)
- [Delete Directory](#delete-directory) - [Delete Directory](#delete-directory)
- [Move File Or Directory](#move-file-or-directory) - [Move File Or Directory](#move-file-or-directory)
- [Read File](#read-file)
- [Write to File](#write-to-file) - [Write to File](#write-to-file)
- [Project Management Operations](#project-management-operations) - [Project Management Operations](#project-management-operations)
- [`project/open`](#projectopen) - [`project/open`](#projectopen)
@ -337,6 +338,27 @@ null;
#### Errors #### Errors
- [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with
underlying data store.
### Read File
Read the provided path and return the contents to stdout.
#### Parameters
```typescript
project-manager --filesystem-read-path {path}
```
### Result
```typescript
null;
```
#### Errors
- [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with - [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with
underlying data store. underlying data store.

View File

@ -23,6 +23,7 @@ object Cli {
val FILESYSTEM_DELETE = "filesystem-delete" val FILESYSTEM_DELETE = "filesystem-delete"
val FILESYSTEM_MOVE_FROM = "filesystem-move-from" val FILESYSTEM_MOVE_FROM = "filesystem-move-from"
val FILESYSTEM_MOVE_TO = "filesystem-move-to" val FILESYSTEM_MOVE_TO = "filesystem-move-to"
val FILESYSTEM_READ_PATH = "filesystem-read-path"
val FILESYSTEM_WRITE_PATH = "filesystem-write-path" val FILESYSTEM_WRITE_PATH = "filesystem-write-path"
object option { object option {
@ -139,6 +140,14 @@ object Cli {
.desc("Move directory. Destination.") .desc("Move directory. Destination.")
.build() .build()
val filesystemReadPath: cli.Option = cli.Option.builder
.hasArg(true)
.numberOfArgs(1)
.argName("path")
.longOpt(FILESYSTEM_READ_PATH)
.desc("Read the contents of the provided file")
.build()
val filesystemWritePath: cli.Option = cli.Option.builder val filesystemWritePath: cli.Option = cli.Option.builder
.hasArg(true) .hasArg(true)
.numberOfArgs(1) .numberOfArgs(1)
@ -165,6 +174,7 @@ object Cli {
.addOption(option.filesystemDelete) .addOption(option.filesystemDelete)
.addOption(option.filesystemMoveFrom) .addOption(option.filesystemMoveFrom)
.addOption(option.filesystemMoveTo) .addOption(option.filesystemMoveTo)
.addOption(option.filesystemReadPath)
.addOption(option.filesystemWritePath) .addOption(option.filesystemWritePath)
/** Parse the command line options. */ /** Parse the command line options. */

View File

@ -15,6 +15,7 @@ import org.enso.projectmanager.boot.command.filesystem.{
FileSystemExistsCommand, FileSystemExistsCommand,
FileSystemListCommand, FileSystemListCommand,
FileSystemMoveDirectoryCommand, FileSystemMoveDirectoryCommand,
FileSystemReadPathCommand,
FileSystemWritePathCommand FileSystemWritePathCommand
} }
import org.enso.projectmanager.boot.command.{CommandHandler, ProjectListCommand} import org.enso.projectmanager.boot.command.{CommandHandler, ProjectListCommand}
@ -254,14 +255,22 @@ object ProjectManager extends ZIOAppDefault with LazyLogging {
to.toFile to.toFile
) )
commandHandler.printJson(fileSystemMoveDirectoryCommand.run) commandHandler.printJson(fileSystemMoveDirectoryCommand.run)
} else if (options.hasOption(Cli.FILESYSTEM_READ_PATH)) {
val path = Paths.get(options.getOptionValue(Cli.FILESYSTEM_READ_PATH))
val fileSystemReadPathCommand =
FileSystemReadPathCommand[ZIO[ZAny, +*, +*]](
config,
path.toFile
)
commandHandler.printJsonErr(fileSystemReadPathCommand.run)
} else if (options.hasOption(Cli.FILESYSTEM_WRITE_PATH)) { } else if (options.hasOption(Cli.FILESYSTEM_WRITE_PATH)) {
val path = Paths.get(options.getOptionValue(Cli.FILESYSTEM_WRITE_PATH)) val path = Paths.get(options.getOptionValue(Cli.FILESYSTEM_WRITE_PATH))
val fileSystemMoveDirectoryCommand = val fileSystemWritePathCommand =
FileSystemWritePathCommand[ZIO[ZAny, +*, +*]]( FileSystemWritePathCommand[ZIO[ZAny, +*, +*]](
config, config,
path.toFile path.toFile
) )
commandHandler.printJson(fileSystemMoveDirectoryCommand.run) commandHandler.printJson(fileSystemWritePathCommand.run)
} else if (options.hasOption(Cli.PROJECT_LIST)) { } else if (options.hasOption(Cli.PROJECT_LIST)) {
val projectsPathOpt = val projectsPathOpt =
Option(options.getOptionValue(Cli.PROJECTS_DIRECTORY)) Option(options.getOptionValue(Cli.PROJECTS_DIRECTORY))

View File

@ -8,6 +8,12 @@ import zio.{Console, ExitCode, ZAny, ZIO}
final class CommandHandler(protocol: Protocol) { final class CommandHandler(protocol: Protocol) {
/** Print the command result to the stdout.
*
* @param result the command result
* @tparam E the error type
* @return the program exit code
*/
def printJson[E: FailureMapper]( def printJson[E: FailureMapper](
result: ZIO[ZAny, E, Any] result: ZIO[ZAny, E, Any]
): ZIO[ZAny, Throwable, ExitCode] = { ): ZIO[ZAny, Throwable, ExitCode] = {
@ -33,6 +39,30 @@ final class CommandHandler(protocol: Protocol) {
.map(_ => SuccessExitCode) .map(_ => SuccessExitCode)
} }
/** Print only the error command result to the stdout suppressing the successful outcome.
*
* @param result the command result
* @tparam E the error type
* @return the program exit code
*/
def printJsonErr[E: FailureMapper](
result: ZIO[ZAny, E, Any]
): ZIO[ZAny, Throwable, ExitCode] = {
consoleLoggingOff *>
result
.foldZIO(
e => {
val error = FailureMapper[E].mapFailure(e)
val errorData =
JsonProtocol.ErrorData(error.code, error.message, error.payload)
val response = JsonProtocol.ResponseError(None, errorData)
Console.printLine(JsonProtocol.encode(response))
},
_ => ZIO.succeed(())
)
.map(_ => SuccessExitCode)
}
private def consoleLoggingOff: ZIO[ZAny, Throwable, Unit] = private def consoleLoggingOff: ZIO[ZAny, Throwable, Unit] =
ZIO.attempt { ZIO.attempt {
val loggerSetup = LoggerSetup.get() val loggerSetup = LoggerSetup.get()

View File

@ -0,0 +1,55 @@
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.desktop.DesktopTrash
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.FileSystemReadPath
import org.enso.projectmanager.service.filesystem.{
FileSystemService,
FileSystemServiceApi,
FileSystemServiceFailure
}
import java.io.{File, OutputStream}
final class FileSystemReadPathCommand[F[+_, +_]: CovariantFlatMap](
service: FileSystemServiceApi[F],
path: File,
output: OutputStream
) {
def run: F[FileSystemServiceFailure, FileSystemReadPath.Result] =
service.read(path, output).map { _ => FileSystemReadPath.Result }
}
object FileSystemReadPathCommand {
def apply[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel: Sync](
config: ProjectManagerConfig,
path: File
): FileSystemReadPathCommand[F] = {
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,
trash
)
val service = new FileSystemService[F](fileSystem, projectRepositoryFactory)
new FileSystemReadPathCommand[F](service, path, System.out)
}
}

View File

@ -1,11 +1,12 @@
package org.enso.projectmanager.infrastructure.file package org.enso.projectmanager.infrastructure.file
import java.io.{File, FileNotFoundException, InputStream}
import java.io.{File, FileNotFoundException, InputStream, OutputStream}
import java.nio.file.{ import java.nio.file.{
AccessDeniedException, AccessDeniedException,
NoSuchFileException, NoSuchFileException,
NotDirectoryException NotDirectoryException
} }
import org.apache.commons.io.{FileExistsException, FileUtils} import org.apache.commons.io.{FileExistsException, FileUtils, IOUtils}
import org.enso.projectmanager.control.effect.syntax._ import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync} import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem.Encoding import org.enso.projectmanager.infrastructure.file.BlockingFileSystem.Encoding
@ -22,17 +23,26 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
ioTimeout: FiniteDuration ioTimeout: FiniteDuration
) extends FileSystem[F] { ) extends FileSystem[F] {
/** Reads the contents of a textual file. /** @inheritdoc */
* override def readTextFile(file: File): F[FileSystemFailure, String] =
* @param file path to the file
* @return either [[FileSystemFailure]] or the content of a file as a String
*/
override def readFile(file: File): F[FileSystemFailure, String] =
Sync[F] Sync[F]
.blockingOp { FileUtils.readFileToString(file, Encoding) } .blockingOp { FileUtils.readFileToString(file, Encoding) }
.mapError(toFsFailure) .mapError(toFsFailure)
.timeoutFail(OperationTimeout)(ioTimeout) .timeoutFail(OperationTimeout)(ioTimeout)
/** @inheritdoc */
override def readFile(
file: File,
output: OutputStream
): F[FileSystemFailure, Int] = {
Sync[F]
.blockingOp {
IOUtils.copy(java.nio.file.Files.newInputStream(file.toPath), output)
}
.mapError(toFsFailure)
.timeoutFail(OperationTimeout)(ioTimeout)
}
/** Writes binary content to a file. /** Writes binary content to a file.
* *
* @param file path to the file * @param file path to the file

View File

@ -1,6 +1,6 @@
package org.enso.projectmanager.infrastructure.file package org.enso.projectmanager.infrastructure.file
import java.io.{File, InputStream} import java.io.{File, InputStream, OutputStream}
/** Represents abstraction for filesystem operations. /** Represents abstraction for filesystem operations.
* *
@ -8,12 +8,20 @@ import java.io.{File, InputStream}
*/ */
trait FileSystem[F[+_, +_]] { trait FileSystem[F[+_, +_]] {
/** Reads the contents of a textual file. /** Read the contents of a textual file.
* *
* @param file path to the file * @param file path to the file
* @return either [[FileSystemFailure]] or the content of a file as a String * @return either [[FileSystemFailure]] or the content of a file as a String
*/ */
def readFile(file: File): F[FileSystemFailure, String] def readTextFile(file: File): F[FileSystemFailure, String]
/** Read the contents of a textual file to the provided output.
*
* @param file path to the file
* @param output the output stream consuming the file contents
* @return either [[FileSystemFailure]] or the number of bytes read
*/
def readFile(file: File, output: OutputStream): F[FileSystemFailure, Int]
/** Writes binary content to a file. /** Writes binary content to a file.
* *

View File

@ -35,7 +35,7 @@ class JsonFileStorage[
*/ */
override def load(): F[LoadFailure, A] = override def load(): F[LoadFailure, A] =
fileSystem fileSystem
.readFile(path) .readTextFile(path)
.mapError(Coproduct[LoadFailure](_)) .mapError(Coproduct[LoadFailure](_))
.flatMap(tryDecodeFileContents) .flatMap(tryDecodeFileContents)

View File

@ -101,6 +101,25 @@ object FileSystemManagementApi {
} }
} }
case object FileSystemReadPath extends Method("filesystem/readPath") {
case class Params(path: File)
type Result = Unused.type
val Result = Unused
implicit val hasParams
: HasParams.Aux[this.type, FileSystemReadPath.Params] =
new HasParams[this.type] {
type Params = FileSystemReadPath.Params
}
implicit val hasResult: HasResult.Aux[this.type, Unused.type] =
new HasResult[this.type] {
type Result = Unused.type
}
}
case object FileSystemWritePath extends Method("filesystem/writePath") { case object FileSystemWritePath extends Method("filesystem/writePath") {
case class Params(path: File) case class Params(path: File)

View File

@ -43,6 +43,7 @@ object JsonRpc {
.registerRequest(FileSystemCreateDirectory) .registerRequest(FileSystemCreateDirectory)
.registerRequest(FileSystemDeleteDirectory) .registerRequest(FileSystemDeleteDirectory)
.registerRequest(FileSystemMoveDirectory) .registerRequest(FileSystemMoveDirectory)
.registerRequest(FileSystemReadPath)
.registerRequest(FileSystemWritePath) .registerRequest(FileSystemWritePath)
.finalized() .finalized()

View File

@ -13,7 +13,7 @@ import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFactor
import org.enso.projectmanager.service.ProjectService import org.enso.projectmanager.service.ProjectService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.{File, InputStream} import java.io.{File, InputStream, OutputStream}
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
@ -83,6 +83,18 @@ class FileSystemService[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel](
FileSystemServiceFailure.FileSystem("Failed to copy path") FileSystemServiceFailure.FileSystem("Failed to copy path")
} }
/** @inheritdoc */
override def read(
path: File,
output: OutputStream
): F[FileSystemServiceFailure, Int] =
fileSystem
.readFile(path, output)
.mapError { error =>
logger.warn("Failed to read path", error)
FileSystemServiceFailure.FileSystem("Failed to read path")
}
/** @inheritdoc */ /** @inheritdoc */
override def write( override def write(
path: File, path: File,

View File

@ -1,6 +1,6 @@
package org.enso.projectmanager.service.filesystem package org.enso.projectmanager.service.filesystem
import java.io.{File, InputStream} import java.io.{File, InputStream, OutputStream}
trait FileSystemServiceApi[F[+_, +_]] { trait FileSystemServiceApi[F[+_, +_]] {
@ -44,10 +44,18 @@ trait FileSystemServiceApi[F[+_, +_]] {
*/ */
def copy(from: File, to: File): F[FileSystemServiceFailure, Unit] def copy(from: File, to: File): F[FileSystemServiceFailure, Unit]
/** Read a file to the provided output.
*
* @param path the file path to write
* @param out the output consuming the file contents
* @return the number of bytes read
*/
def read(path: File, out: OutputStream): F[FileSystemServiceFailure, Int]
/** Writes a file /** Writes a file
* *
* @param path the file path to write * @param path the file path to write
* @param bytes the file contents * @param in the file contents
*/ */
def write(path: File, in: InputStream): F[FileSystemServiceFailure, Unit] def write(path: File, in: InputStream): F[FileSystemServiceFailure, Unit]
} }