Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for the file/checksum Endpoint #1787

Merged
merged 4 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
([#1759](https://github.com/enso-org/enso/pull/1759)). This allows the IDE to
get information about the running project in contexts where the project
manager isn't available or works differently.
- Added the `file/checksum` endpoint to the language server
([#1787](https://github.com/enso-org/enso/pull/1787)). This allows the IDE to
verify the integrity of files that it has transferred. The checksum is
calculated in a streaming fashion so the checksummed file need not be resident
in memory all at once.

## Libraries

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ class FileManager(
.map(FileManagerProtocol.InfoFileResult)
.pipeTo(sender())
()

case FileManagerProtocol.ChecksumRequest(path) =>
val getChecksum = for {
rootPath <- IO.fromEither(config.findContentRoot(path.rootId))
checksum <- fs.digest(path.toFile(rootPath))
} yield checksum
exec
.execTimed(config.fileManager.timeout, getChecksum)
.map(FileManagerProtocol.ChecksumResponse)
.pipeTo(sender())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ object FileManagerApi {
}
}

case object ChecksumFile extends Method("file/checksum") {
case class Params(path: Path)
case class Result(checksum: String)

implicit val hasParams = new HasParams[this.type] {
type Params = ChecksumFile.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = ChecksumFile.Result
}
}

case object EventFile extends Method("file/event") {

case class Params(path: Path, kind: FileEventKind)
Expand All @@ -163,6 +175,8 @@ object FileManagerApi {

case object NotDirectoryError extends Error(1006, "Path is not a directory")

case object NotFileError extends Error(1007, "Path is not a file")

case object CannotDecodeError
extends Error(1010, "Cannot decode the project configuration")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,18 @@ object FileManagerProtocol {
* @param result either file system failure or attributes
*/
case class InfoFileResult(result: Either[FileSystemFailure, FileAttributes])

/** Requests that the Language Server provide the checksum of the specified
* file system object
*
* @param path to the file system object
*/
case class ChecksumRequest(path: Path)

/** Returns the checksum of the file system object in question.
*
* @param checksum either a FS failure or the checksum as a base64-encoded
* string
*/
case class ChecksumResponse(checksum: Either[FileSystemFailure, String])
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package org.enso.languageserver.filemanager

import java.io.{File, FileNotFoundException}
import java.nio.file._
import java.nio.file.attribute.BasicFileAttributes

import org.apache.commons.io.{FileExistsException, FileUtils}
import org.bouncycastle.util.encoders.Hex
import org.enso.languageserver.effect.BlockingIO
import zio._
import zio.blocking.effectBlocking

import java.io.{File, FileNotFoundException}
import java.nio.file._
import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest
import scala.collection.mutable
import scala.util.Using

/** File manipulation facility.
*
Expand Down Expand Up @@ -221,14 +223,45 @@ class FileSystem extends FileSystemApi[BlockingIO] {
IO.fail(FileNotFound)
}

/** Returns the digest of the file at the provided `path`
*
* @param path the path to the filesystem object
* @return either [[FileSystemFailure]] or the file checksum
*/
override def digest(path: File): BlockingIO[FileSystemFailure, String] = {
if (path.isFile) {
effectBlocking {
val messageDigest = MessageDigest.getInstance("SHA3-224")
Using.resource(
Files.newInputStream(path.toPath, StandardOpenOption.READ)
) { stream =>
val tenMb = 1 * 1024 * 1024 * 10
var currentBytes = stream.readNBytes(tenMb)

while (currentBytes.nonEmpty) {
messageDigest.update(currentBytes)
currentBytes = stream.readNBytes(tenMb)
}

Hex.toHexString(messageDigest.digest())
}
}.mapError(errorHandling)
} else {
if (path.exists()) {
IO.fail(NotFile)
} else {
IO.fail(FileNotFound)
}
}
}

private val errorHandling: Throwable => FileSystemFailure = {
case _: FileNotFoundException => FileNotFound
case _: NoSuchFileException => FileNotFound
case _: FileExistsException => FileExists
case _: AccessDeniedException => AccessDenied
case ex => GenericFileSystemFailure(ex.getMessage)
}

}

object FileSystem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ trait FileSystemApi[F[_, _]] {
* @return either [[FileSystemFailure]] or file attributes
*/
def info(path: File): F[FileSystemFailure, Attributes]

/** Returns the digest for the file at the provided path.
*
* @param path the path to the filesystem object
* @return either [[FileSystemFailure]] or the file checksum
*/
def digest(path: File): F[FileSystemFailure, String]
}

object FileSystemApi {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ case object OperationTimeout extends FileSystemFailure
*/
case object NotDirectory extends FileSystemFailure

/** Signal that the provided path is not a file. */
case object NotFile extends FileSystemFailure

/** Signals file system specific errors.
*
* @param reason a reason of failure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.enso.languageserver.filemanager.FileManagerApi.{
FileNotFoundError,
FileSystemError,
NotDirectoryError,
NotFileError,
OperationTimeoutError
}
import org.enso.jsonrpc.Error
Expand All @@ -26,6 +27,7 @@ object FileSystemFailureMapper {
case FileExists => FileExistsError
case OperationTimeout => OperationTimeoutError
case NotDirectory => NotDirectoryError
case NotFile => NotFileError
case GenericFileSystemFailure(reason) => FileSystemError(reason)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ class JsonConnectionController(
ListFile -> file.ListFileHandler.props(requestTimeout, fileManager),
TreeFile -> file.TreeFileHandler.props(requestTimeout, fileManager),
InfoFile -> file.InfoFileHandler.props(requestTimeout, fileManager),
ChecksumFile -> file.ChecksumFileHandler
.props(requestTimeout, fileManager),
ExecutionContextCreate -> executioncontext.CreateHandler
.props(requestTimeout, contextRegistry, rpcSession),
ExecutionContextDestroy -> executioncontext.DestroyHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ object JsonRpc {
.registerRequest(ListFile)
.registerRequest(TreeFile)
.registerRequest(InfoFile)
.registerRequest(ChecksumFile)
.registerRequest(RedirectStandardOutput)
.registerRequest(RedirectStandardError)
.registerRequest(SuppressStandardOutput)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.enso.languageserver.requesthandler.file

import akka.actor.{Actor, ActorRef, Cancellable, Props, Status}
import com.typesafe.scalalogging.LazyLogging
import org.enso.jsonrpc.Errors.RequestTimeout
import org.enso.jsonrpc._
import org.enso.languageserver.filemanager.FileManagerApi.ChecksumFile
import org.enso.languageserver.filemanager.{
FileManagerProtocol,
FileSystemFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.logger.masking.MaskedString

import scala.concurrent.duration.FiniteDuration

/** A request handler for the `file/checksum` command.
*
* @param requestTimeout a request timeout
* @param fileManager a file system manager actor
*/
class ChecksumFileHandler(
requestTimeout: FiniteDuration,
fileManager: ActorRef
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher

override def receive: Receive = requestStage

private def requestStage: Receive = {
case Request(ChecksumFile, id, params: ChecksumFile.Params) =>
fileManager ! FileManagerProtocol.ChecksumRequest(params.path)
val cancellable = context.system.scheduler.scheduleOnce(
requestTimeout,
self,
RequestTimeout
)
context.become(responseStage(id, sender(), cancellable))
}

private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
logger.error(
"Failure during [{}] operation: {}",
ChecksumFile,
MaskedString(ex.getMessage)
)
replyTo ! ResponseError(Some(id), Errors.ServiceError)
cancellable.cancel()
context.stop(self)

case RequestTimeout =>
logger.error("Request [{}] timed out.", id)
replyTo ! ResponseError(Some(id), Errors.RequestTimeout)
context.stop(self)

case FileManagerProtocol.ChecksumResponse(Left(failure)) =>
replyTo ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
)
cancellable.cancel()
context.stop(self)

case FileManagerProtocol.ChecksumResponse(Right(result)) =>
replyTo ! ResponseResult(ChecksumFile, id, ChecksumFile.Result(result))
cancellable.cancel()
context.stop(self)
}
}
object ChecksumFileHandler {

/** Creates a configuration object used to create a [[ChecksumFileHandler]].
*
* @param timeout a request timeout
* @param fileManager the file manager actor
* @return an actor for handling checksum file commands
*/
def props(timeout: FiniteDuration, fileManager: ActorRef): Props = Props(
new ChecksumFileHandler(timeout, fileManager)
)
}
2 changes: 1 addition & 1 deletion engine/language-server/src/main/schema/binary_protocol.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ table ChecksumBytesCommand {
// The checksum of the specified bytes.
table ChecksumBytesReply {
// The segment in a file to checksum.
checksum : EnsoDigest;
checksum : EnsoDigest (required);
}

// A SHA3-224 digest.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package org.enso.languageserver.filemanager

import java.nio.file.{Files, Path, Paths}
import java.nio.file.attribute.BasicFileAttributes

import org.apache.commons.io.FileUtils
import org.bouncycastle.util.encoders.Hex
import org.enso.languageserver.effect.Effects
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import scala.io.Source
import scala.collection.mutable.ArrayBuffer

Expand Down Expand Up @@ -640,6 +642,30 @@ class FileSystemSpec extends AnyFlatSpec with Matchers with Effects {
result shouldBe Left(FileNotFound)
}

it should "return the correct checksum when the target is a file" in new TestCtx {
val path = Paths.get(testDirPath.toString, "a.txt")
val fileContents = "Hello, Enso!"
createFileContaining(fileContents, path)

val expectedDigest =
MessageDigest.getInstance("SHA3-224").digest(Files.readAllBytes(path))
val expectedDigestString = Hex.toHexString(expectedDigest)

val result = objectUnderTest.digest(path.toFile).unsafeRunSync()
result shouldBe Right(expectedDigestString)
}

it should "return an error if the provided path is not a file" in new TestCtx {
val result = objectUnderTest.digest(testDirPath.toFile).unsafeRunSync()
result shouldBe Left(NotFile)
}

it should "return a FileNotFound error when getting the checksum if the file does not exist" in new TestCtx {
val path = Paths.get(testDirPath.toString, "nonexistent.txt")
val result = objectUnderTest.digest(path.toFile).unsafeRunSync()
result shouldBe Left(FileNotFound)
}

def readTxtFile(path: Path): String = {
val buffer = Source.fromFile(path.toFile)
val content = buffer.getLines().mkString
Expand All @@ -648,8 +674,19 @@ class FileSystemSpec extends AnyFlatSpec with Matchers with Effects {
}

def createEmptyFile(path: Path): Path = {
Files.createDirectories(path.getParent())
Files.createDirectories(path.getParent)
Files.createFile(path)
}

def createFileContaining(contents: String, path: Path): Path = {
createFileContaining(contents.getBytes(StandardCharsets.UTF_8), path)
}

def createFileContaining(contents: Array[Byte], path: Path): Path = {
Files.createDirectories(path.getParent)
Files.createFile(path)
Files.write(path, contents)
path
}

trait TestCtx {
Expand Down
Loading