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 3 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
iamrecursion marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -204,7 +204,7 @@ class BinaryConnectionController(
WRITE_FILE_CMD -> WriteBinaryFileHandler
.props(requestTimeout, fileManager, outboundChannel),
READ_FILE_CMD -> ReadBinaryFileHandler
.props(requestTimeout, fileManager, outboundChannel)
.props(requestTimeout, fileManager, outboundChannel),
iamrecursion marked this conversation as resolved.
Show resolved Hide resolved
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,10 @@ import akka.util.Timeout
import com.typesafe.scalalogging.LazyLogging
import org.enso.jsonrpc._
import org.enso.languageserver.boot.resource.InitializationComponent
import org.enso.languageserver.capability.CapabilityApi.{
AcquireCapability,
ForceReleaseCapability,
GrantCapability,
ReleaseCapability
}
import org.enso.languageserver.capability.CapabilityApi.{AcquireCapability, ForceReleaseCapability, GrantCapability, ReleaseCapability}
import org.enso.languageserver.capability.CapabilityProtocol
import org.enso.languageserver.data.Config
import org.enso.languageserver.event.{
JsonSessionInitialized,
JsonSessionTerminated
}
import org.enso.languageserver.event.{JsonSessionInitialized, JsonSessionTerminated}
import org.enso.languageserver.filemanager.FileManagerApi._
import org.enso.languageserver.filemanager.PathWatcherProtocol
import org.enso.languageserver.io.InputOutputApi._
Expand All @@ -28,37 +20,19 @@ import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
import org.enso.languageserver.requesthandler._
import org.enso.languageserver.requesthandler.capability._
import org.enso.languageserver.requesthandler.io._
import org.enso.languageserver.requesthandler.monitoring.{
InitialPingHandler,
PingHandler
}
import org.enso.languageserver.requesthandler.monitoring.{InitialPingHandler, PingHandler}
import org.enso.languageserver.requesthandler.refactoring.RenameProjectHandler
import org.enso.languageserver.requesthandler.session.InitProtocolConnectionHandler
import org.enso.languageserver.requesthandler.text._
import org.enso.languageserver.requesthandler.visualisation.{
AttachVisualisationHandler,
DetachVisualisationHandler,
ExecuteExpressionHandler,
ModifyVisualisationHandler
}
import org.enso.languageserver.requesthandler.visualisation.{AttachVisualisationHandler, DetachVisualisationHandler, ExecuteExpressionHandler, ModifyVisualisationHandler}
iamrecursion marked this conversation as resolved.
Show resolved Hide resolved
import org.enso.languageserver.requesthandler.workspace.ProjectInfoHandler
import org.enso.languageserver.runtime.ContextRegistryProtocol
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation,
DetachVisualisation,
ExecuteExpression,
ModifyVisualisation
}
import org.enso.languageserver.runtime.VisualisationApi.{AttachVisualisation, DetachVisualisation, ExecuteExpression, ModifyVisualisation}
import org.enso.languageserver.search.SearchApi._
import org.enso.languageserver.search.{SearchApi, SearchProtocol}
import org.enso.languageserver.session.JsonSession
import org.enso.languageserver.session.SessionApi.{
InitProtocolConnection,
ResourcesInitializationError,
SessionAlreadyInitialisedError,
SessionNotInitialisedError
}
import org.enso.languageserver.session.SessionApi.{InitProtocolConnection, ResourcesInitializationError, SessionAlreadyInitialisedError, SessionNotInitialisedError}
import org.enso.languageserver.text.TextApi._
import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.util.UnhandledLogging
Expand Down Expand Up @@ -320,6 +294,7 @@ 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,89 @@
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.annotation.unused
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(
@unused requestTimeout: FiniteDuration,
@unused fileManager: ActorRef
iamrecursion marked this conversation as resolved.
Show resolved Hide resolved
) 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
Loading