File and directory creation for LS (#560)

This commit is contained in:
Łukasz Olczak 2020-02-28 11:37:42 +01:00 committed by GitHub
parent fe471314ec
commit 0b22606fa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 330 additions and 39 deletions

View File

@ -1171,7 +1171,10 @@ null
```
##### Errors
TBC
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable file-system error.
- [`ContentRootNotFoundError`](#contentrootnotfounderror) to signal that the requested content root cannot be found.
- [`AccessDeniedError`](#accessdeniederror) to signal that a user doesn't have access to a resource.
#### `file/delete`
This request asks the file manager to delete the specified file system object.

View File

@ -7,8 +7,11 @@ import akka.pattern.ask
import akka.util.Timeout
import org.enso.languageserver.ClientApi._
import org.enso.languageserver.data.{CapabilityRegistration, Client}
import org.enso.languageserver.filemanager.FileManagerApi.{FileRead, _}
import org.enso.languageserver.filemanager.FileManagerProtocol.FileWriteResult
import org.enso.languageserver.filemanager.FileManagerApi._
import org.enso.languageserver.filemanager.FileManagerProtocol.{
CreateFileResult,
WriteFileResult
}
import org.enso.languageserver.filemanager.{
FileManagerProtocol,
FileSystemFailureMapper
@ -63,8 +66,9 @@ object ClientApi {
val protocol: Protocol = Protocol.empty
.registerRequest(AcquireCapability)
.registerRequest(ReleaseCapability)
.registerRequest(FileWrite)
.registerRequest(FileRead)
.registerRequest(WriteFile)
.registerRequest(ReadFile)
.registerRequest(CreateFile)
.registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability)
@ -119,26 +123,28 @@ class ClientController(
server ! LanguageProtocol.ReleaseCapability(clientId, params.id)
sender ! ResponseResult(ReleaseCapability, id, Unused)
case Request(FileWrite, id, params: FileWrite.Params) =>
case Request(WriteFile, id, params: WriteFile.Params) =>
writeFile(webActor, id, params)
case Request(FileRead, id, params: FileRead.Params) =>
case Request(ReadFile, id, params: ReadFile.Params) =>
readFile(webActor, id, params)
case Request(CreateFile, id, params: CreateFile.Params) =>
createFile(webActor, id, params)
}
private def readFile(
webActor: ActorRef,
id: Id,
params: FileRead.Params
params: ReadFile.Params
): Unit = {
(server ? FileManagerProtocol.FileRead(params.path)).onComplete {
(server ? FileManagerProtocol.ReadFile(params.path)).onComplete {
case Success(
FileManagerProtocol.FileReadResult(Right(content: String))
FileManagerProtocol.ReadFileResult(Right(content: String))
) =>
webActor ! ResponseResult(FileRead, id, FileRead.Result(content))
webActor ! ResponseResult(ReadFile, id, ReadFile.Result(content))
case Success(FileManagerProtocol.FileReadResult(Left(failure))) =>
case Success(FileManagerProtocol.ReadFileResult(Left(failure))) =>
webActor ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
@ -153,14 +159,14 @@ class ClientController(
private def writeFile(
webActor: ActorRef,
id: Id,
params: FileWrite.Params
params: WriteFile.Params
): Unit = {
(server ? FileManagerProtocol.FileWrite(params.path, params.contents))
(server ? FileManagerProtocol.WriteFile(params.path, params.contents))
.onComplete {
case Success(FileWriteResult(Right(()))) =>
webActor ! ResponseResult(FileWrite, id, Unused)
case Success(WriteFileResult(Right(()))) =>
webActor ! ResponseResult(WriteFile, id, Unused)
case Success(FileWriteResult(Left(failure))) =>
case Success(WriteFileResult(Left(failure))) =>
webActor ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
@ -172,4 +178,26 @@ class ClientController(
}
}
private def createFile(
webActor: ActorRef,
id: Id,
params: CreateFile.Params
): Unit = {
(server ? FileManagerProtocol.CreateFile(params.`object`))
.onComplete {
case Success(CreateFileResult(Right(()))) =>
webActor ! ResponseResult(CreateFile, id, Unused)
case Success(CreateFileResult(Left(failure))) =>
webActor ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
)
case Failure(th) =>
log.error("An exception occurred during creating a file", th)
webActor ! ResponseError(Some(id), ServiceError)
}
}
}

View File

@ -1,17 +1,10 @@
package org.enso.languageserver
import java.io.File
import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
import cats.effect.IO
import org.enso.languageserver.data._
import org.enso.languageserver.filemanager.FileManagerProtocol.{
FileRead,
FileReadResult,
FileWrite,
FileWriteResult
}
import org.enso.languageserver.filemanager.{FileSystemApi, FileSystemFailure}
import org.enso.languageserver.filemanager.FileManagerProtocol._
import org.enso.languageserver.filemanager.{FileSystemApi, FileSystemObject}
object LanguageProtocol {
@ -128,23 +121,42 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
initialized(config, env.releaseCapability(clientId, capabilityId))
)
case FileWrite(path, content) =>
case WriteFile(path, content) =>
val result =
for {
rootPath <- config.findContentRoot(path.rootId)
_ <- fs.write(path.toFile(rootPath), content).unsafeRunSync()
} yield ()
sender ! FileWriteResult(result)
sender ! WriteFileResult(result)
case FileRead(path) =>
case ReadFile(path) =>
val result =
for {
rootPath <- config.findContentRoot(path.rootId)
content <- fs.read(path.toFile(rootPath)).unsafeRunSync()
} yield content
sender ! FileReadResult(result)
sender ! ReadFileResult(result)
case CreateFile(FileSystemObject.File(name, path)) =>
val result =
for {
rootPath <- config.findContentRoot(path.rootId)
_ <- fs.createFile(path.toFile(rootPath, name)).unsafeRunSync()
} yield ()
sender ! CreateFileResult(result)
case CreateFile(FileSystemObject.Directory(name, path)) =>
val result =
for {
rootPath <- config.findContentRoot(path.rootId)
_ <- fs.createDirectory(path.toFile(rootPath, name)).unsafeRunSync()
} yield ()
sender ! CreateFileResult(result)
}
/* Note [Usage of unsafe methods]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -15,29 +15,41 @@ import org.enso.languageserver.jsonrpc.{
*/
object FileManagerApi {
case object FileWrite extends Method("file/write") {
case object WriteFile extends Method("file/write") {
case class Params(path: Path, contents: String)
implicit val hasParams = new HasParams[this.type] {
type Params = FileWrite.Params
type Params = WriteFile.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = Unused.type
}
}
case object FileRead extends Method("file/read") {
case object ReadFile extends Method("file/read") {
case class Params(path: Path)
case class Result(contents: String)
implicit val hasParams = new HasParams[this.type] {
type Params = FileRead.Params
type Params = ReadFile.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = FileRead.Result
type Result = ReadFile.Result
}
}
case object CreateFile extends Method("file/create") {
case class Params(`object`: FileSystemObject)
implicit val hasParams = new HasParams[this.type] {
type Params = CreateFile.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = Unused.type
}
}

View File

@ -8,27 +8,41 @@ object FileManagerProtocol {
* @param path a path to a file
* @param content a textual content
*/
case class FileWrite(path: Path, content: String)
case class WriteFile(path: Path, content: String)
/**
* Signals file manipulation status.
*
* @param result either file system failure or unit representing success
*/
case class FileWriteResult(result: Either[FileSystemFailure, Unit])
case class WriteFileResult(result: Either[FileSystemFailure, Unit])
/**
* Requests the Language Server read a file.
*
* @param path a path to a file
*/
case class FileRead(path: Path)
case class ReadFile(path: Path)
/**
* Returns a result of reading a file.
*
* @param result either file system failure or content of a file
*/
case class FileReadResult(result: Either[FileSystemFailure, String])
case class ReadFileResult(result: Either[FileSystemFailure, String])
/**
* Requests the Language Server create a file system object.
*
* @param `object` a file system object
*/
case class CreateFile(`object`: FileSystemObject)
/**
* Returns a result of creating a file system object.
*
* @param result either file system failure or unit representing success
*/
case class CreateFileResult(result: Either[FileSystemFailure, Unit])
}

View File

@ -3,6 +3,7 @@ package org.enso.languageserver.filemanager
import java.io.{File, FileNotFoundException, IOException}
import java.nio.file._
import cats.data.EitherT
import cats.effect.Sync
import cats.implicits._
import org.apache.commons.io.FileUtils
@ -48,6 +49,50 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] {
.leftMap(errorHandling)
}
/**
* Creates an empty file with parent directory.
*
* @param file path to the file
* @return
*/
override def createFile(file: File): F[Either[FileSystemFailure, Unit]] = {
val op =
for {
_ <- EitherT { createDirectory(file.getParentFile) }
_ <- EitherT { createEmptyFile(file) }
} yield ()
op.value
}
private def createEmptyFile(file: File): F[Either[FileSystemFailure, Unit]] =
Sync[F].delay {
Either
.catchOnly[IOException] {
file.createNewFile()
}
.leftMap(errorHandling)
.map(_ => ())
}
/**
* Creates a directory, including any necessary but nonexistent parent
* directories.
*
* @param file path to the file
* @return
*/
override def createDirectory(
file: File
): F[Either[FileSystemFailure, Unit]] =
Sync[F].delay {
Either
.catchOnly[IOException] {
FileUtils.forceMkdir(file)
}
.leftMap(errorHandling)
}
private val errorHandling: IOException => FileSystemFailure = {
case _: FileNotFoundException => FileNotFound
case _: AccessDeniedException => AccessDenied

View File

@ -29,4 +29,21 @@ trait FileSystemApi[F[_]] {
*/
def read(file: File): F[Either[FileSystemFailure, String]]
/**
* Creates an empty file with parent directory.
*
* @param file path to the file
* @return
*/
def createFile(file: File): F[Either[FileSystemFailure, Unit]]
/**
* Creates a directory, including any necessary but nonexistent parent
* directories.
*
* @param file path to the file
* @return
*/
def createDirectory(file: File): F[Either[FileSystemFailure, Unit]]
}

View File

@ -0,0 +1,74 @@
package org.enso.languageserver.filemanager
import io.circe.generic.auto._
import io.circe.syntax._
import io.circe.{Decoder, Encoder, Json}
/**
* A representation of filesystem object.
*/
sealed trait FileSystemObject
object FileSystemObject {
/**
* Represents a directory.
*
* @param name a name of the directory
* @param path a path to the directory
*/
case class Directory(name: String, path: Path) extends FileSystemObject
/**
* Represents a file.
*
* @param name a name of the file
* @param path a path to the file
*/
case class File(name: String, path: Path) extends FileSystemObject
private val TypeField = "type"
private val NameField = "name"
private val PathField = "path"
private val FileType = "File"
private val DirectoryType = "Directory"
implicit val fsoDecoder: Decoder[FileSystemObject] =
Decoder.instance { cursor =>
cursor.downField(TypeField).as[String].flatMap {
case FileType =>
for {
name <- cursor.downField(NameField).as[String]
path <- cursor.downField(PathField).as[Path]
} yield File(name, path)
case DirectoryType =>
for {
name <- cursor.downField(NameField).as[String]
path <- cursor.downField(PathField).as[Path]
} yield Directory(name, path)
}
}
implicit val fsoEncoder: Encoder[FileSystemObject] =
Encoder.instance[FileSystemObject] {
case Directory(name, path) =>
Json.obj(
TypeField -> DirectoryType.asJson,
NameField -> name.asJson,
PathField -> path.asJson
)
case File(name, path) =>
Json.obj(
TypeField -> FileType.asJson,
NameField -> name.asJson,
PathField -> path.asJson
)
}
}

View File

@ -16,4 +16,9 @@ case class Path(rootId: UUID, segments: List[String]) {
case (parent, child) => new File(parent, child)
}
def toFile(rootPath: File, fileName: String): File = {
val parentDir = toFile(rootPath)
new File(parentDir, fileName)
}
}

View File

@ -351,6 +351,66 @@ class WebSocketServerTest
""")
}
"create a file" in {
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 7,
"params": {
"object": {
"type": "File",
"name": "bar.txt",
"path": {
"rootId": $testContentRootId,
"segments": [ "foo1" ]
}
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 7,
"result": null
}
""")
val file = Paths.get(testContentRoot.toString, "foo1", "bar.txt").toFile
file.isFile shouldBe true
}
"create a directory" in {
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 7,
"params": {
"object": {
"type": "Directory",
"name": "baz",
"path": {
"rootId": $testContentRootId,
"segments": [ "foo1" ]
}
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 7,
"result": null
}
""")
val file = Paths.get(testContentRoot.toString, "foo1", "baz").toFile
file.isDirectory shouldBe true
}
}
class WsTestClient(address: String) {

View File

@ -68,6 +68,27 @@ class FileSystemSpec extends AnyFlatSpec with Matchers {
result shouldBe Right(content)
}
it should "create a directory" in new TestCtx {
//given
val path = Paths.get(testDirPath.toString, "foo", "bar")
//when
val result = objectUnderTest.createDirectory(path.toFile).unsafeRunSync()
//then
result shouldBe Right(())
path.toFile.isDirectory shouldBe true
}
it should "create an empty file" in new TestCtx {
//given
val path = Paths.get(testDirPath.toString, "foo", "bar", "baz.txt")
//when
val result = objectUnderTest.createFile(path.toFile).unsafeRunSync()
//then
result shouldBe Right(())
path.toFile.getParentFile.isDirectory shouldBe true
path.toFile.isFile shouldBe true
}
def readTxtFile(path: Path): String = {
val buffer = Source.fromFile(path.toFile)
val content = buffer.getLines().mkString