Implement byte-based file operations (#1795)

This commit is contained in:
Ara Adkins 2021-06-11 14:48:28 +01:00 committed by GitHub
parent 0b363e3e85
commit 90c020d666
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2763 additions and 794 deletions

View File

@ -36,6 +36,9 @@
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.
- Added support for reading and writing byte ranges in files remotely
([#1795](https://github.com/enso-org/enso/pull/1795)). This allows the IDE to
transfer files to a remote back-end in a streaming fashion.
## Libraries

View File

@ -1644,8 +1644,12 @@ This method will create a file if no file is present at `path`.
length of the file.
- The `byteOffset` property is zero-indexed. To append to the file you begin
writing at index `file.length`.
- If `byteOffset` is less than the length of the file and `overwriteExisting` is
set, it will truncate the file to length `byteOffset + bytes.length`.
- If `byteOffset > file.length`, the bytes in the range
`[file.length, byteOffset)` will be filled with null bytes.
`[file.length, byteOffset)` will be filled with null bytes. Please note that,
in this case, the checksum in the response will also be calculated on the null
bytes.
#### Parameters
@ -4055,7 +4059,7 @@ Signals that the requested file read was out of bounds for the file's size.
"code" : 1009
"message" : "Read is out of bounds for the file"
"data" : {
fileLength : 0
"fileLength" : 0
}
}
```

View File

@ -23,6 +23,7 @@ A few key requirements:
<!-- MarkdownTOC levels="2,3" autolink="true" indent=" " -->
- [Control](#control)
- [Concurrency](#concurrency)
- [UX](#ux)
<!-- /MarkdownTOC -->
@ -50,6 +51,19 @@ used.
Resumption of transfers is also handled by the IDE, which may keep track of what
portions of a file have been written or read.
### Concurrency
The language server natively supports running these file operations in parallel
as it spawns a separate request-handler actor for each operation. It does,
however, not provide any _intrinsic_ guarantees to its operation. As _all_ file
operations are evaluated in parallel, coordinating them for consistency is up to
the IDE.
For example, if you want to write bytes to a file `f1` and then checksum the
resulting file, you need to wait for the `WriteBytesReply` to come back before
sending `file/checksum(f1)`. Otherwise, there is no guarantee that the write has
completed by the time the checksum is calculated.
## UX
The IDE wants to be able to provide two major UX benefits to users as part of

View File

@ -4,6 +4,7 @@ import akka.actor.{Actor, Props}
import akka.routing.SmallestMailboxPool
import akka.pattern.pipe
import com.typesafe.scalalogging.LazyLogging
import org.bouncycastle.util.encoders.Hex
import org.enso.languageserver.effect._
import org.enso.languageserver.data.Config
import org.enso.languageserver.monitoring.MonitoringProtocol.{Ping, Pong}
@ -195,14 +196,48 @@ class FileManager(
.pipeTo(sender())
()
case FileManagerProtocol.ChecksumRequest(path) =>
case FileManagerProtocol.ChecksumFileRequest(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)
.map(x =>
FileManagerProtocol.ChecksumFileResponse(
x.map(digest => Hex.toHexString(digest.bytes))
)
)
.pipeTo(sender())
case FileManagerProtocol.ChecksumBytesRequest(segment) =>
val getChecksum = for {
rootPath <- IO.fromEither(config.findContentRoot(segment.path.rootId))
checksum <- fs.digestBytes(segment.toApiSegment(rootPath))
} yield checksum
exec
.execTimed(config.fileManager.timeout, getChecksum)
.map(x => FileManagerProtocol.ChecksumBytesResponse(x.map(_.bytes)))
.pipeTo(sender())
case FileManagerProtocol.WriteBytesRequest(path, off, overwrite, bytes) =>
val doWrite = for {
rootPath <- IO.fromEither(config.findContentRoot(path.rootId))
response <- fs.writeBytes(path.toFile(rootPath), off, overwrite, bytes)
} yield response
exec
.execTimed(config.fileManager.timeout, doWrite)
.map(x => FileManagerProtocol.WriteBytesResponse(x.map(_.bytes)))
.pipeTo(sender())
case FileManagerProtocol.ReadBytesRequest(segment) =>
val doRead = for {
rootPath <- IO.fromEither(config.findContentRoot(segment.path.rootId))
response <- fs.readBytes(segment.toApiSegment(rootPath))
} yield response
exec
.execTimed(config.fileManager.timeout, doRead)
.map(FileManagerProtocol.ReadBytesResponse)
.pipeTo(sender())
}
}

View File

@ -1,5 +1,7 @@
package org.enso.languageserver.filemanager
import io.circe.Json
import io.circe.literal.JsonStringContext
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
/** The file manager JSON RPC API provided by the language server.
@ -177,6 +179,19 @@ object FileManagerApi {
case object NotFileError extends Error(1007, "Path is not a file")
case object CannotOverwriteError
extends Error(
1008,
"Cannot overwrite the file without `overwriteExisting` set"
)
case class ReadOutOfBoundsError(length: Long)
extends Error(1009, "Read is out of bounds for the file") {
override def payload: Option[Json] = Some(
json""" { "fileLength" : $length }"""
)
}
case object CannotDecodeError
extends Error(1010, "Cannot decode the project configuration")

View File

@ -182,12 +182,82 @@ object FileManagerProtocol {
*
* @param path to the file system object
*/
case class ChecksumRequest(path: Path)
case class ChecksumFileRequest(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])
case class ChecksumFileResponse(checksum: Either[FileSystemFailure, String])
/** Requests that the file manager provide the checksum of the specified bytes
* in a file.
*
* @param segment a description of the bytes in a file to checksum.
*/
case class ChecksumBytesRequest(segment: Data.FileSegment)
/** Returns the checksum of the bytes in question.
*
* @param checksum either a FS failure or the checksum as an array of bytes
*/
case class ChecksumBytesResponse(
checksum: Either[FileSystemFailure, Array[Byte]]
)
/** Requests that the file manager writes the provided `bytes` to the file at
* `path`.
*
* @param path the file to write to
* @param byteOffset the offset in the file to begin writing from
* @param overwriteExisting whether or not the request can overwrite existing
* data
* @param bytes the bytes to write
*/
case class WriteBytesRequest(
path: Path,
byteOffset: Long,
overwriteExisting: Boolean,
bytes: Array[Byte]
)
/** Returns the checksum of the bytes that were written to disk.
*
* @param checksum either a FS failure or the checksum as an array of bytes
*/
case class WriteBytesResponse(
checksum: Either[FileSystemFailure, Array[Byte]]
)
/** Requests to read the bytes in the file identified by `segment`.
*
* @param segment an identification of where the bytes should be read from
*/
case class ReadBytesRequest(segment: Data.FileSegment)
/** Returns the requested bytes and their checksum.
*
* @param result either a FS failure or the checksum and corresponding bytes
* that were read
*/
case class ReadBytesResponse(
result: Either[FileSystemFailure, FileSystemApi.ReadBytesResult]
)
/** Data types for the protocol. */
object Data {
/** A representation of a segment in the file.
*
* @param path the path to the file in question
* @param byteOffset the byte offset in the file to start from
* @param length the number of bytes in the segment
*/
case class FileSegment(path: Path, byteOffset: Long, length: Long) {
def toApiSegment(rootPath: File): FileSystemApi.FileSegment = {
FileSystemApi.FileSegment(path.toFile(rootPath), byteOffset, length)
}
}
}
}

View File

@ -1,12 +1,12 @@
package org.enso.languageserver.filemanager
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.io.{File, FileNotFoundException, RandomAccessFile}
import java.nio.ByteBuffer
import java.nio.file._
import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest
@ -14,11 +14,14 @@ import scala.collection.mutable
import scala.util.Using
/** File manipulation facility.
*
* @tparam F represents target monad
*/
class FileSystem extends FileSystemApi[BlockingIO] {
private val tenMb: Int = 1 * 1024 * 1024 * 10
/** The stride used by the [[FileSystem]] when processing a file in chunks. */
val fileChunkSize: Int = tenMb
import FileSystemApi._
/** Writes textual content to a file.
@ -228,22 +231,157 @@ class FileSystem extends FileSystemApi[BlockingIO] {
* @param path the path to the filesystem object
* @return either [[FileSystemFailure]] or the file checksum
*/
override def digest(path: File): BlockingIO[FileSystemFailure, String] = {
override def digest(path: File): BlockingIO[FileSystemFailure, SHA3_224] = {
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)
var currentBytes = stream.readNBytes(fileChunkSize)
while (currentBytes.nonEmpty) {
messageDigest.update(currentBytes)
currentBytes = stream.readNBytes(tenMb)
currentBytes = stream.readNBytes(fileChunkSize)
}
Hex.toHexString(messageDigest.digest())
SHA3_224(messageDigest.digest())
}
}.mapError(errorHandling)
} else {
if (path.exists()) {
IO.fail(NotFile)
} else {
IO.fail(FileNotFound)
}
}
}
/** Returns the digest of the bytes described by `segment`.
*
* @param segment a description of the portion of a file to checksum
* @return either [[FileSystemFailure]] or the bytes representing the checksum
*/
override def digestBytes(
segment: FileSegment
): BlockingIO[FileSystemFailure, SHA3_224] = {
val path = segment.path
if (path.isFile) {
effectBlocking {
val messageDigest = MessageDigest.getInstance("SHA3-224")
Using.resource(
Files.newInputStream(path.toPath, StandardOpenOption.READ)
) { stream =>
val fileLength = Files.size(path.toPath)
val lastByteIndex = fileLength - 1
val lastSegIndex = segment.byteOffset + segment.length
if (segment.byteOffset > lastByteIndex || lastSegIndex > lastByteIndex) {
throw FileSystem.ReadOutOfBounds(fileLength)
}
var bytePosition = stream.skip(segment.byteOffset)
var bytesToRead = segment.length
do {
val readSize = Math.min(bytesToRead, fileChunkSize.toLong).toInt
val bytes = stream.readNBytes(readSize)
bytePosition += bytes.length
bytesToRead -= bytes.length
messageDigest.update(bytes)
} while (bytesToRead > 0)
SHA3_224(messageDigest.digest())
}
}.mapError(errorHandling)
} else {
if (path.exists()) {
IO.fail(NotFile)
} else {
IO.fail(FileNotFound)
}
}
}
override def writeBytes(
path: File,
byteOffset: Long,
overwriteExisting: Boolean,
bytes: Array[Byte]
): BlockingIO[FileSystemFailure, SHA3_224] = {
if (path.isDirectory) {
IO.fail(NotFile)
} else {
effectBlocking {
Using.resource(new RandomAccessFile(path, "rw")) { file =>
Using.resource(file.getChannel) { chan =>
val lock = chan.lock()
try {
val fileSize = chan.size()
val messageDigest = MessageDigest.getInstance("SHA3-224")
if (byteOffset < fileSize) {
if (overwriteExisting) {
chan.truncate(byteOffset)
} else {
throw FileSystem.CannotOverwrite
}
} else if (byteOffset > fileSize) {
chan.position(fileSize)
var nullBytesLeft = byteOffset - fileSize
do {
val numBytesInRound =
Math.min(nullBytesLeft, fileChunkSize.toLong)
val bytes = Array.fill(numBytesInRound.toInt)(0x0.toByte)
val bytesBuf = ByteBuffer.wrap(bytes)
messageDigest.update(bytes)
chan.write(bytesBuf)
nullBytesLeft -= numBytesInRound
} while (nullBytesLeft > 0)
}
chan.position(chan.size())
messageDigest.update(bytes)
chan.write(ByteBuffer.wrap(bytes))
SHA3_224(messageDigest.digest())
} finally {
lock.release()
}
}
}
}.mapError(errorHandling)
}
}
override def readBytes(
segment: FileSegment
): BlockingIO[FileSystemFailure, ReadBytesResult] = {
val path = segment.path
if (path.isFile) {
effectBlocking {
Using.resource(
Files.newInputStream(path.toPath, StandardOpenOption.READ)
) { stream =>
stream.skip(segment.byteOffset)
val fileSize = Files.size(path.toPath)
val lastByteIndex = fileSize - 1
if (lastByteIndex < segment.byteOffset) {
throw FileSystem.ReadOutOfBounds(fileSize)
}
val bytesToRead = segment.length
val bytes = stream.readNBytes(bytesToRead.toInt)
val digest = MessageDigest.getInstance("SHA3-224").digest(bytes)
ReadBytesResult(SHA3_224(digest), bytes)
}
}.mapError(errorHandling)
} else {
@ -256,16 +394,29 @@ class FileSystem extends FileSystemApi[BlockingIO] {
}
private val errorHandling: Throwable => FileSystemFailure = {
case _: FileNotFoundException => FileNotFound
case _: NoSuchFileException => FileNotFound
case _: FileExistsException => FileExists
case _: AccessDeniedException => AccessDenied
case ex => GenericFileSystemFailure(ex.getMessage)
case _: FileNotFoundException => FileNotFound
case _: NoSuchFileException => FileNotFound
case _: FileExistsException => FileExists
case _: AccessDeniedException => AccessDenied
case FileSystem.ReadOutOfBounds(l) => ReadOutOfBounds(l)
case FileSystem.CannotOverwrite => CannotOverwrite
case ex => GenericFileSystemFailure(ex.getMessage)
}
}
object FileSystem {
/** An exception for when a file segment read goes out of bounds.
*
* @param length the true length of the file
*/
case class ReadOutOfBounds(length: Long) extends Throwable
/** An exception for when overwriting would be required but the corresponding
* flag is not set.
*/
case object CannotOverwrite extends Throwable
import FileSystemApi._
/** Represent a depth limit when recursively traversing a directory.

View File

@ -4,7 +4,6 @@ import java.io.File
import java.nio.file.Path
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
import java.time.{OffsetDateTime, ZoneOffset}
import org.enso.languageserver.effect.BlockingIO
import scala.collection.mutable.ArrayBuffer
@ -131,15 +130,66 @@ trait FileSystemApi[F[_, _]] {
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, SHA3_224]
/** Returns the digest for the bytes in the file described by `segment`.
*
* @param segment a description of the portion of a file to checksum
* @return either [[FileSystemFailure]] or the bytes representing the checksum
*/
def digestBytes(segment: FileSegment): F[FileSystemFailure, SHA3_224]
/** Writes the provided `bytes` to the file at `path` on disk.
*
* @param path the path to the file into which the bytes will be written
* @param byteOffset the offset in the file to start writing from
* @param overwriteExisting whether existing bytes can be overwritten
* @param bytes the bytes to write to the file
* @return either a [[FileSystemFailure]] or the checksum of the `bytes` as
* they were written to disk
*/
def writeBytes(
path: File,
byteOffset: Long,
overwriteExisting: Boolean,
bytes: Array[Byte]
): F[FileSystemFailure, SHA3_224]
/** Reads the bytes specified by `segment` from the specified `segment.file`.
*
* @param path the path to the filesystem object
* @return either [[FileSystemFailure]] or the file checksum
* @param segment a description of the portion of a file to checksum
* @return either [[FileSystemFailure]] or the bytes representing the checksum
*/
def digest(path: File): F[FileSystemFailure, String]
def readBytes(segment: FileSegment): F[FileSystemFailure, ReadBytesResult]
}
object FileSystemApi {
/** A SHA3-224 digest on the filesystem.
*
* @param bytes the bytes that represent the value of the digest
*/
case class SHA3_224(bytes: Array[Byte])
/** The bytes read from the file.
*
* @param checksum the checksum of `bytes`
* @param bytes the bytes that were read
*/
case class ReadBytesResult(checksum: SHA3_224, bytes: Array[Byte])
/** A representation of a segment in the file.
*
* @param path the path to the file in question
* @param byteOffset the byte offset in the file to start from
* @param length the number of bytes in the segment
*/
case class FileSegment(path: File, byteOffset: Long, length: Long)
/** An object representing abstract file system entry.
*/
sealed trait Entry {

View File

@ -2,7 +2,11 @@ package org.enso.languageserver.filemanager
/** Represents file system failures.
*/
sealed trait FileSystemFailure
sealed trait FileSystemFailure {
/** Tells whether the error has additional data. */
def hasData: Boolean = false
}
/** Informs that the requested content root cannot be found.
*/
@ -31,8 +35,21 @@ case object NotDirectory extends FileSystemFailure
/** Signal that the provided path is not a file. */
case object NotFile extends FileSystemFailure
/** Signals that the file cannot be overwritten. */
case object CannotOverwrite extends FileSystemFailure
/** Signals that the provided file cannot be read at the requested offset.
*
* @param fileLength the actual length of the file.
*/
case class ReadOutOfBounds(fileLength: Long) extends FileSystemFailure {
override def hasData: Boolean = true
}
/** Signals file system specific errors.
*
* @param reason a reason of failure
*/
case class GenericFileSystemFailure(reason: String) extends FileSystemFailure
case class GenericFileSystemFailure(reason: String) extends FileSystemFailure {
override def hasData: Boolean = true
}

View File

@ -1,15 +1,7 @@
package org.enso.languageserver.filemanager
import org.enso.languageserver.filemanager.FileManagerApi.{
ContentRootNotFoundError,
FileExistsError,
FileNotFoundError,
FileSystemError,
NotDirectoryError,
NotFileError,
OperationTimeoutError
}
import org.enso.jsonrpc.Error
import org.enso.languageserver.filemanager.FileManagerApi._
import org.enso.languageserver.protocol.json.ErrorApi
object FileSystemFailureMapper {
@ -28,6 +20,8 @@ object FileSystemFailureMapper {
case OperationTimeout => OperationTimeoutError
case NotDirectory => NotDirectoryError
case NotFile => NotFileError
case CannotOverwrite => CannotOverwriteError
case ReadOutOfBounds(l) => ReadOutOfBoundsError(l)
case GenericFileSystemFailure(reason) => FileSystemError(reason)
}

View File

@ -1,8 +1,5 @@
package org.enso.languageserver.protocol.binary
import java.nio.ByteBuffer
import java.util.UUID
import akka.actor.{Actor, ActorRef, Props, Stash}
import akka.http.scaladsl.model.RemoteAddress
import com.google.flatbuffers.FlatBufferBuilder
@ -17,21 +14,14 @@ import org.enso.languageserver.http.server.BinaryWebSocketControlProtocol.{
OutboundStreamEstablished
}
import org.enso.languageserver.protocol.binary.BinaryConnectionController.InboundPayloadType
import org.enso.languageserver.protocol.binary.InboundPayload.{
INIT_SESSION_CMD,
READ_FILE_CMD,
WRITE_FILE_CMD
}
import org.enso.languageserver.protocol.binary.InboundPayload._
import org.enso.languageserver.protocol.binary.factory.{
ErrorFactory,
OutboundMessageFactory,
SuccessReplyFactory,
VisualisationUpdateFactory
}
import org.enso.languageserver.requesthandler.file.{
ReadBinaryFileHandler,
WriteBinaryFileHandler
}
import org.enso.languageserver.requesthandler.file._
import org.enso.languageserver.runtime.ContextRegistryProtocol.VisualisationUpdate
import org.enso.languageserver.session.BinarySession
import org.enso.languageserver.util.UnhandledLogging
@ -42,6 +32,8 @@ import org.enso.languageserver.util.binary.DecodingFailure.{
GenericDecodingFailure
}
import java.nio.ByteBuffer
import java.util.UUID
import scala.annotation.unused
import scala.concurrent.duration._
@ -204,6 +196,12 @@ class BinaryConnectionController(
WRITE_FILE_CMD -> WriteBinaryFileHandler
.props(requestTimeout, fileManager, outboundChannel),
READ_FILE_CMD -> ReadBinaryFileHandler
.props(requestTimeout, fileManager, outboundChannel),
CHECKSUM_BYTES_CMD -> ChecksumBytesHandler
.props(requestTimeout, fileManager, outboundChannel),
WRITE_BYTES_CMD -> WriteBytesHandler
.props(requestTimeout, fileManager, outboundChannel),
READ_BYTES_CMD -> ReadBytesHandler
.props(requestTimeout, fileManager, outboundChannel)
)
}

View File

@ -0,0 +1,39 @@
package org.enso.languageserver.protocol.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.{
ChecksumBytesReply,
EnsoUUID,
OutboundPayload
}
import java.nio.ByteBuffer
import java.util.UUID
object ChecksumBytesReplyFactory {
/** Creates a [[ChecksumBytesReply]] inside a [[FlatBufferBuilder]].
*
* @param checksum the checksum value for the reply
* @param correlationId an identifier used to correlate a response with a
* request
* @return a FlatBuffer representation of the reply
*/
def create(checksum: Array[Byte], correlationId: EnsoUUID): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val digestOffset = EnsoDigestFactory.create(checksum)
val replyOffset =
ChecksumBytesReply.createChecksumBytesReply(builder, digestOffset)
val outMsg = OutboundMessageFactory.create(
UUID.randomUUID(),
Some(correlationId),
OutboundPayload.CHECKSUM_BYTES_REPLY,
replyOffset
)
builder.finish(outMsg)
builder.dataBuffer()
}
}

View File

@ -0,0 +1,19 @@
package org.enso.languageserver.protocol.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.EnsoDigest
object EnsoDigestFactory {
/** Create a new EnsoDigest.
*
* @param bytes the bytes of the digest
* @param builder the flatbuffer builder in which the digest is created
* @return the offset of the digest in `builder`
*/
def create(bytes: Array[Byte])(implicit builder: FlatBufferBuilder): Int = {
val bytesOff = builder.createByteVector(bytes)
EnsoDigest.createEnsoDigest(builder, bytesOff)
}
}

View File

@ -7,7 +7,8 @@ import org.enso.languageserver.protocol.binary.{
EnsoUUID,
Error,
ErrorPayload,
OutboundPayload
OutboundPayload,
ReadOutOfBoundsError
}
object ErrorFactory {
@ -33,12 +34,41 @@ object ErrorFactory {
def createServiceError(
maybeCorrelationId: Option[EnsoUUID] = None
): ByteBuffer =
createGenericError(0, "Service error", maybeCorrelationId)
createGenericError(
0,
"Service error",
maybeCorrelationId = maybeCorrelationId
)
/** Creates an error representing a read that is out of bounds in a file with
* length `actualLength`.
*
* @param actualLength the actual length of the file
* @param maybeCorrelationId an optional correlation ID for the error
* @return a FlatBuffer representation of the error
*/
def createReadOutOfBoundsError(
actualLength: Long,
maybeCorrelationId: Option[EnsoUUID] = None
): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val payloadData =
ReadOutOfBoundsError.createReadOutOfBoundsError(builder, actualLength)
createGenericErrorWithBuilder(
1009,
"Read is out of bounds for the file",
Some(ErrorData(ErrorPayload.READ_OOB, payloadData)),
maybeCorrelationId = maybeCorrelationId
)
}
/** Creates a generic error inside a [[FlatBufferBuilder]].
*
* @param code an error code
* @param message an error textual message
* @param data optional error payload
* @param maybeCorrelationId an optional correlation id used to correlate
* a response with a request
* @return an FlatBuffer representation of the created error
@ -46,17 +76,49 @@ object ErrorFactory {
def createGenericError(
code: Int,
message: String,
data: Option[ErrorData] = None,
maybeCorrelationId: Option[EnsoUUID] = None
): ByteBuffer = {
implicit val builder = new FlatBufferBuilder(1024)
val offset =
Error.createError(
builder,
code,
builder.createString(message),
ErrorPayload.NONE,
0
)
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
createGenericErrorWithBuilder(code, message, data, maybeCorrelationId)
}
/** Creates a generic error inside the provided [[FlatBufferBuilder]].
*
* @param code an error code
* @param message an error textual message
* @param data optional error payload
* @param maybeCorrelationId an optional correlation id used to correlate
* a response with a request
* @param builder the builder to use for creating the error
* @return a FlatBuffer representation of the created error
*/
def createGenericErrorWithBuilder(
code: Int,
message: String,
data: Option[ErrorData] = None,
maybeCorrelationId: Option[EnsoUUID] = None
)(implicit builder: FlatBufferBuilder): ByteBuffer = {
val offset = data match {
case Some(d) =>
Error.createError(
builder,
code,
builder.createString(message),
d.payloadVariant,
d.payloadData
)
case None =>
Error.createError(
builder,
code,
builder.createString(message),
ErrorPayload.NONE,
0
)
}
val outMsg = OutboundMessageFactory.create(
UUID.randomUUID(),
maybeCorrelationId,
@ -67,4 +129,10 @@ object ErrorFactory {
builder.dataBuffer()
}
/** Stores additional data for the error.
*
* @param payloadVariant the variant set in the payload
* @param payloadData the data for that variant
*/
case class ErrorData(payloadVariant: Byte, payloadData: Int)
}

View File

@ -15,7 +15,7 @@ object FileContentsReplyFactory {
* @param contents the binary contents of a file
* @param correlationId correlation id used to correlate a response with a
* request
* @return an FlatBuffer representation of the created error
* @return an FlatBuffer representation of the reply
*/
def createPacket(
contents: Array[Byte],

View File

@ -0,0 +1,45 @@
package org.enso.languageserver.protocol.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.{
EnsoUUID,
OutboundPayload,
ReadBytesReply
}
import java.nio.ByteBuffer
import java.util.UUID
object ReadBytesReplyFactory {
/** Creates a [[ReadBytesReply]] inside a [[FlatBufferBuilder]].
*
* @param checksum the checksum value of the read bytes
* @param bytes the bytes that were read
* @param correlationId an identifier used to correlate a response with a
* request
* @return a FlatBuffer representation of the reply
*/
def create(
checksum: Array[Byte],
bytes: Array[Byte],
correlationId: EnsoUUID
): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val digestOffset = EnsoDigestFactory.create(checksum)
val bytesOffset = builder.createByteVector(bytes)
val replyOffset =
ReadBytesReply.createReadBytesReply(builder, digestOffset, bytesOffset)
val outMsg = OutboundMessageFactory.create(
UUID.randomUUID(),
Some(correlationId),
OutboundPayload.READ_BYTES_REPLY,
replyOffset
)
builder.finish(outMsg)
builder.dataBuffer()
}
}

View File

@ -0,0 +1,39 @@
package org.enso.languageserver.protocol.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.{
EnsoUUID,
OutboundPayload,
WriteBytesReply
}
import java.nio.ByteBuffer
import java.util.UUID
object WriteBytesReplyFactory {
/** Creates a [[WriteBytesReply]] inside a [[FlatBufferBuilder]].
*
* @param checksum the checksum value of the written bytes
* @param correlationId an identifier used to correlate a response with a
* request
* @return a FlatBuffer representation of the reply
*/
def create(checksum: Array[Byte], correlationId: EnsoUUID): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val digestOffset = EnsoDigestFactory.create(checksum)
val replyOffset =
WriteBytesReply.createWriteBytesReply(builder, digestOffset)
val outMsg = OutboundMessageFactory.create(
UUID.randomUUID(),
Some(correlationId),
OutboundPayload.WRITE_BYTES_REPLY,
replyOffset
)
builder.finish(outMsg)
builder.dataBuffer()
}
}

View File

@ -0,0 +1,146 @@
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.languageserver.filemanager.{
FileManagerProtocol,
FileSystemFailureMapper,
ReadOutOfBounds
}
import org.enso.languageserver.protocol.binary.factory.{
ChecksumBytesReplyFactory,
ErrorFactory
}
import org.enso.languageserver.protocol.binary.{
ChecksumBytesCommand,
EnsoUUID,
FileSegment,
InboundMessage
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.util.file.PathUtils
import org.enso.logger.masking.MaskedString
import scala.concurrent.duration.FiniteDuration
/** A handler for a checksum bytes request.
*
* @param requestTimeout a request timeout
* @param fileManager a reference to the file-manager actor
* @param replyTo the actor to reply to
*/
class ChecksumBytesHandler(
requestTimeout: FiniteDuration,
fileManager: ActorRef,
replyTo: ActorRef
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = { case msg: InboundMessage =>
val payload =
msg.payload(new ChecksumBytesCommand).asInstanceOf[ChecksumBytesCommand]
val segment = payload.segment
fileManager ! FileManagerProtocol.ChecksumBytesRequest(
ChecksumBytesHandler.convertFileSegment(segment)
)
val cancellable = context.system.scheduler.scheduleOnce(
requestTimeout,
self,
RequestTimeout
)
context.become(responseStage(msg.messageId(), cancellable))
}
private def responseStage(
requestId: EnsoUUID,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
logger.error(
"Failure during the ChecksumBytes operation: {}",
MaskedString(ex.getMessage)
)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
logger.error("Request ChecksumBytes [{}] timed out.", requestId)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
context.stop(self)
case FileManagerProtocol.ChecksumBytesResponse(Left(failure))
if failure.hasData =>
failure match {
case ReadOutOfBounds(fileLength) =>
val response =
ErrorFactory.createReadOutOfBoundsError(fileLength, Some(requestId))
replyTo ! response
cancellable.cancel()
context.stop(self)
case _ =>
logger.error("The impossible happened in request [{}].", requestId)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
context.stop(self)
}
case FileManagerProtocol.ChecksumBytesResponse(Left(failure))
if !failure.hasData =>
val error = FileSystemFailureMapper.mapFailure(failure)
val response = ErrorFactory.createGenericError(
error.code,
error.message,
maybeCorrelationId = Some(requestId)
)
replyTo ! response
cancellable.cancel()
context.stop(self)
case FileManagerProtocol.ChecksumBytesResponse(Right(checksum)) =>
val response = ChecksumBytesReplyFactory.create(checksum, requestId)
replyTo ! response
cancellable.cancel()
context.stop(self)
}
}
object ChecksumBytesHandler {
/** Creates a configuration object used to create a [[ChecksumBytesHandler]].
*
* @param timeout the request timeout
* @param fileManager the file system manager actor
* @param replyTo the outbound channel delivering replies to the client
* @return a configuration object
*/
def props(
timeout: FiniteDuration,
fileManager: ActorRef,
replyTo: ActorRef
): Props = {
Props(new ChecksumBytesHandler(timeout, fileManager, replyTo))
}
/** Converts from a binary file segment to a protocol one.
*
* @param segment the segment to convert
* @return `segment` using protocol types
*/
def convertFileSegment(
segment: FileSegment
): FileManagerProtocol.Data.FileSegment = {
FileManagerProtocol.Data.FileSegment(
PathUtils.convertBinaryPath(segment.path),
segment.byteOffset(),
segment.length()
)
}
}

View File

@ -31,7 +31,7 @@ class ChecksumFileHandler(
private def requestStage: Receive = {
case Request(ChecksumFile, id, params: ChecksumFile.Params) =>
fileManager ! FileManagerProtocol.ChecksumRequest(params.path)
fileManager ! FileManagerProtocol.ChecksumFileRequest(params.path)
val cancellable = context.system.scheduler.scheduleOnce(
requestTimeout,
self,
@ -60,7 +60,7 @@ class ChecksumFileHandler(
replyTo ! ResponseError(Some(id), Errors.RequestTimeout)
context.stop(self)
case FileManagerProtocol.ChecksumResponse(Left(failure)) =>
case FileManagerProtocol.ChecksumFileResponse(Left(failure)) =>
replyTo ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
@ -68,7 +68,7 @@ class ChecksumFileHandler(
cancellable.cancel()
context.stop(self)
case FileManagerProtocol.ChecksumResponse(Right(result)) =>
case FileManagerProtocol.ChecksumFileResponse(Right(result)) =>
replyTo ! ResponseResult(ChecksumFile, id, ChecksumFile.Result(result))
cancellable.cancel()
context.stop(self)

View File

@ -75,7 +75,7 @@ class ReadBinaryFileHandler(
val packet = ErrorFactory.createGenericError(
error.code,
error.message,
Some(requestId)
maybeCorrelationId = Some(requestId)
)
replyTo ! packet
cancellable.cancel()

View File

@ -0,0 +1,133 @@
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.languageserver.filemanager.{
FileManagerProtocol,
FileSystemFailureMapper,
ReadOutOfBounds
}
import org.enso.languageserver.protocol.binary.factory.{
ErrorFactory,
ReadBytesReplyFactory
}
import org.enso.languageserver.protocol.binary.{
EnsoUUID,
InboundMessage,
ReadBytesCommand
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.logger.masking.MaskedString
import scala.concurrent.duration.FiniteDuration
/** A handler for a read bytes request
*
* @param requestTimeout a request timeout
* @param fileManager a reference to the file-manager actor
* @param replyTo the actor to reply to
*/
class ReadBytesHandler(
requestTimeout: FiniteDuration,
fileManager: ActorRef,
replyTo: ActorRef
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = { case msg: InboundMessage =>
val payload =
msg.payload(new ReadBytesCommand).asInstanceOf[ReadBytesCommand]
val segment = payload.segment
fileManager ! FileManagerProtocol.ReadBytesRequest(
ChecksumBytesHandler.convertFileSegment(segment)
)
val cancellable = context.system.scheduler.scheduleOnce(
requestTimeout,
self,
RequestTimeout
)
context.become(responseStage(msg.messageId(), cancellable))
}
private def responseStage(
requestId: EnsoUUID,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
logger.error(
"Failure during the ChecksumBytes operation: {}",
MaskedString(ex.getMessage)
)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
logger.error("Request ChecksumBytes [{}] timed out.", requestId)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
context.stop(self)
case FileManagerProtocol.ReadBytesResponse(Left(failure))
if failure.hasData =>
failure match {
case ReadOutOfBounds(fileLength) =>
val response =
ErrorFactory.createReadOutOfBoundsError(fileLength, Some(requestId))
replyTo ! response
cancellable.cancel()
context.stop(self)
case _ =>
logger.error("The impossible happened in request [{}].", requestId)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
context.stop(self)
}
case FileManagerProtocol.ReadBytesResponse(Left(failure))
if !failure.hasData =>
val error = FileSystemFailureMapper.mapFailure(failure)
val response = ErrorFactory.createGenericError(
error.code,
error.message,
maybeCorrelationId = Some(requestId)
)
replyTo ! response
cancellable.cancel()
context.stop(self)
case FileManagerProtocol.ReadBytesResponse(Right(readBytesResult)) =>
val response = ReadBytesReplyFactory.create(
readBytesResult.checksum.bytes,
readBytesResult.bytes,
requestId
)
replyTo ! response
cancellable.cancel()
context.stop(self)
}
}
object ReadBytesHandler {
/** Creates a configuration object used to create a [[ReadBytesHandler]].
*
* @param timeout the request timeout
* @param fileManager the file system manager actor
* @param replyTo the outbound channel delivering replies to the client
* @return a configuration object
*/
def props(
timeout: FiniteDuration,
fileManager: ActorRef,
replyTo: ActorRef
): Props = {
Props(new ReadBytesHandler(timeout, fileManager, replyTo))
}
}

View File

@ -78,7 +78,7 @@ class WriteBinaryFileHandler(
val packet = ErrorFactory.createGenericError(
error.code,
error.message,
Some(requestId)
maybeCorrelationId = Some(requestId)
)
replyTo ! packet
cancellable.cancel()

View File

@ -0,0 +1,117 @@
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.languageserver.filemanager.{
FileManagerProtocol,
FileSystemFailureMapper
}
import org.enso.languageserver.protocol.binary.factory.{
ErrorFactory,
WriteBytesReplyFactory
}
import org.enso.languageserver.protocol.binary.{
EnsoUUID,
InboundMessage,
WriteBytesCommand
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.util.file.PathUtils
import org.enso.logger.masking.MaskedString
import scala.concurrent.duration.FiniteDuration
/** A handler for a write bytes request
*
* @param requestTimeout a request timeout
* @param fileManager a reference to the file-manager actor
* @param replyTo the actor to reply to
*/
class WriteBytesHandler(
requestTimeout: FiniteDuration,
fileManager: ActorRef,
replyTo: ActorRef
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = { case msg: InboundMessage =>
val payload =
msg.payload(new WriteBytesCommand).asInstanceOf[WriteBytesCommand]
val byteBuf = payload.bytesAsByteBuffer()
val bytes = new Array[Byte](byteBuf.remaining())
byteBuf.get(bytes)
fileManager ! FileManagerProtocol.WriteBytesRequest(
PathUtils.convertBinaryPath(payload.path()),
payload.byteOffset(),
payload.overwriteExisting(),
bytes
)
val cancellable = context.system.scheduler.scheduleOnce(
requestTimeout,
self,
RequestTimeout
)
context.become(responseStage(msg.messageId(), cancellable))
}
private def responseStage(
requestId: EnsoUUID,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
logger.error(
"Failure during the WriteBytes operation: {}",
MaskedString(ex.getMessage)
)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
logger.error("Request WriteBytes [{}] timed out.", requestId)
val response = ErrorFactory.createServiceError(Some(requestId))
replyTo ! response
context.stop(self)
case FileManagerProtocol.WriteBytesResponse(Left(failure)) =>
val error = FileSystemFailureMapper.mapFailure(failure)
val response = ErrorFactory.createGenericError(
error.code,
error.message,
maybeCorrelationId = Some(requestId)
)
replyTo ! response
cancellable.cancel()
context.stop(self)
case FileManagerProtocol.WriteBytesResponse(Right(checksum)) =>
val response = WriteBytesReplyFactory.create(checksum, requestId)
replyTo ! response
cancellable.cancel()
context.stop(self)
}
}
object WriteBytesHandler {
/** Creates a configuration object used to create a [[WriteBytesHandler]].
*
* @param timeout the request timeout
* @param fileManager the file system manager actor
* @param replyTo the outbound channel delivering replies to the client
* @return a configuration object
*/
def props(
timeout: FiniteDuration,
fileManager: ActorRef,
replyTo: ActorRef
): Props = {
Props(new WriteBytesHandler(timeout, fileManager, replyTo))
}
}

View File

@ -1,25 +1,16 @@
package org.enso.languageserver.websocket.binary
import java.io.File
import java.nio.ByteBuffer
import java.util.UUID
import com.google.flatbuffers.FlatBufferBuilder
import org.apache.commons.io.FileUtils
import org.enso.languageserver.protocol.binary.{
InboundPayload,
OutboundMessage,
OutboundPayload
}
import org.enso.languageserver.protocol.binary.FileContentsReply
import org.enso.languageserver.websocket.binary.factory.{
InboundMessageFactory,
PathFactory,
ReadFileCommandFactory,
WriteFileCommandFactory
}
import org.enso.languageserver.protocol.binary._
import org.enso.languageserver.websocket.binary.factory._
import org.enso.testkit.FlakySpec
import java.io.File
import java.nio.ByteBuffer
import java.nio.file.Files
import java.security.MessageDigest
import java.util.UUID
import scala.io.Source
class BinaryFileManipulationTest extends BaseBinaryServerTest with FlakySpec {
@ -93,17 +84,523 @@ class BinaryFileManipulationTest extends BaseBinaryServerTest with FlakySpec {
}
"A ChecksumBytesCommand" must {
"Return the checksum for the provided byte range" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val contents = Array[Byte](65, 66, 67) //ABC
FileUtils.writeByteArrayToFile(barFile, contents)
val expectedChecksum = ByteBuffer.wrap(
MessageDigest.getInstance("SHA3-224").digest(contents.slice(0, 2))
)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val checksumBytesCmd = createChecksumBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
byteOffset = 0,
length = 2
)
client.send(checksumBytesCmd)
val checksumResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
checksumResponse
.payloadType() shouldEqual OutboundPayload.CHECKSUM_BYTES_REPLY
val payload = checksumResponse
.payload(new ChecksumBytesReply)
.asInstanceOf[ChecksumBytesReply]
val digest = payload.checksum().bytesAsByteBuffer()
digest shouldEqual expectedChecksum
Files.delete(barFile.toPath)
}
"Return a `FileNotFound` error if the file does not exist" in {
val requestId = UUID.randomUUID()
val filename = "does_not_exist.bin"
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val checksumBytesCmd = createChecksumBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
byteOffset = 0,
length = 2
)
client.send(checksumBytesCmd)
val checksumResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
checksumResponse.payloadType() shouldEqual OutboundPayload.ERROR
val payload = checksumResponse
.payload(new Error)
.asInstanceOf[Error]
payload.code() shouldEqual 1003
payload.message() shouldEqual "File not found"
}
"Return a `ReadOutOfBounds` error if the byte range is out of bounds" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val contents = Array[Byte](65, 66, 67) //ABC
FileUtils.writeByteArrayToFile(barFile, contents)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val checksumBytesCmd = createChecksumBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
byteOffset = 3,
length = 2
)
client.send(checksumBytesCmd)
val checksumResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
checksumResponse.payloadType() shouldEqual OutboundPayload.ERROR
val payload = checksumResponse
.payload(new Error)
.asInstanceOf[Error]
payload.code() shouldEqual 1009
payload.message() shouldEqual "Read is out of bounds for the file"
payload.dataType() shouldEqual ErrorPayload.READ_OOB
val data = payload
.data(new ReadOutOfBoundsError)
.asInstanceOf[ReadOutOfBoundsError]
data.fileLength() shouldEqual 3
}
"Return a `NotFile` error if the provided path is not a file" in {
val requestId = UUID.randomUUID()
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val checksumBytesCmd = createChecksumBytesCommandPacket(
requestId,
Seq(),
testContentRootId,
byteOffset = 3,
length = 2
)
client.send(checksumBytesCmd)
val checksumResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
checksumResponse.payloadType() shouldEqual OutboundPayload.ERROR
val payload = checksumResponse
.payload(new Error)
.asInstanceOf[Error]
payload.code() shouldEqual 1007
payload.message() shouldEqual "Path is not a file"
}
}
"A WriteBytesCommand" must {
"Write the provided bytes to the specified file" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val contents = Array[Byte](65, 66, 67) //ABC
FileUtils.writeByteArrayToFile(barFile, contents)
val newBytes = Array[Byte](65, 66, 67)
val expectedChecksum =
ByteBuffer.wrap(MessageDigest.getInstance("SHA3-224").digest(newBytes))
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val writeBytesCommand = createWriteBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
3L,
newBytes,
overwriteExisting = false
)
client.send(writeBytesCommand)
val writeResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
writeResponse
.payloadType() shouldEqual OutboundPayload.WRITE_BYTES_REPLY
val payload = writeResponse
.payload(new WriteBytesReply)
.asInstanceOf[WriteBytesReply]
val digest = payload.checksum().bytesAsByteBuffer()
digest shouldEqual expectedChecksum
val expectedBytes = Array[Byte](65, 66, 67, 65, 66, 67)
val writtenBytes = Files.readAllBytes(barFile.toPath)
writtenBytes should contain theSameElementsAs expectedBytes
Files.delete(barFile.toPath)
}
"Create the file from scratch if it doesn't exist" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val newBytes = Array[Byte](65, 66, 67)
val expectedChecksum =
MessageDigest.getInstance("SHA3-224").digest(newBytes)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val writeBytesCommand = createWriteBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
0L,
newBytes,
overwriteExisting = false
)
client.send(writeBytesCommand)
val writeResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
writeResponse
.payloadType() shouldEqual OutboundPayload.WRITE_BYTES_REPLY
val payload = writeResponse
.payload(new WriteBytesReply)
.asInstanceOf[WriteBytesReply]
val digest = payload.checksum().bytesAsByteBuffer()
val digestBytes = new Array[Byte](digest.remaining())
digest.get(digestBytes)
digestBytes shouldEqual expectedChecksum
val expectedBytes = Array[Byte](65, 66, 67)
val writtenBytes = Files.readAllBytes(barFile.toPath)
writtenBytes should contain theSameElementsAs expectedBytes
Files.delete(barFile.toPath)
}
"Return a `CannotOverwrite` error if `byteOffset < file.length`" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val contents = Array[Byte](65, 66, 67) //ABC
FileUtils.writeByteArrayToFile(barFile, contents)
val newBytes = Array[Byte](65, 66, 67)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val writeBytesCommand = createWriteBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
1L,
newBytes,
overwriteExisting = false
)
client.send(writeBytesCommand)
val writeResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
writeResponse
.payloadType() shouldEqual OutboundPayload.ERROR
val payload = writeResponse
.payload(new Error)
.asInstanceOf[Error]
payload.dataType() shouldEqual ErrorPayload.NONE
payload.code() shouldEqual 1008
payload.message() shouldEqual "Cannot overwrite the file without `overwriteExisting` set"
Files.delete(barFile.toPath)
}
"Return a `NotFile` error if the provided path is not a file" in {
val requestId = UUID.randomUUID()
val newBytes = Array[Byte](65, 66, 67)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val writeBytesCommand = createWriteBytesCommandPacket(
requestId,
Seq(),
testContentRootId,
1L,
newBytes,
overwriteExisting = false
)
client.send(writeBytesCommand)
val writeResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
writeResponse
.payloadType() shouldEqual OutboundPayload.ERROR
val payload = writeResponse
.payload(new Error)
.asInstanceOf[Error]
payload.dataType() shouldEqual ErrorPayload.NONE
payload.code() shouldEqual 1007
payload.message() shouldEqual "Path is not a file"
}
}
"A ReadBytesCommand" must {
"Read the specified bytes from the file" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val contents = Array[Byte](65, 66, 67) //ABC
FileUtils.writeByteArrayToFile(barFile, contents)
val expectedBytes = Array[Byte](65, 66)
val expectedChecksum =
MessageDigest.getInstance("SHA3-224").digest(expectedBytes)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val readBytesCommand = createReadBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
0L,
2L
)
client.send(readBytesCommand)
val readResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
readResponse
.payloadType() shouldEqual OutboundPayload.READ_BYTES_REPLY
val payload = readResponse
.payload(new ReadBytesReply)
.asInstanceOf[ReadBytesReply]
val digestBuffer = payload.checksum().bytesAsByteBuffer()
val digest = new Array[Byte](payload.checksum().bytesLength())
digestBuffer.get(digest)
val bytesBuffer = payload.bytesAsByteBuffer()
val bytes = new Array[Byte](payload.bytesLength())
bytesBuffer.get(bytes)
bytes should contain theSameElementsAs expectedBytes
digest should contain theSameElementsAs expectedChecksum
Files.delete(barFile.toPath)
}
"Return a `FileNotFound` error if the file does not exist" in {
val requestId = UUID.randomUUID()
val filename = "does_not_exist.bin"
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val readBytesCommand = createReadBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
0L,
2L
)
client.send(readBytesCommand)
val readResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
readResponse
.payloadType() shouldEqual OutboundPayload.ERROR
val payload = readResponse
.payload(new Error)
.asInstanceOf[Error]
payload.code shouldEqual 1003
payload.message shouldEqual "File not found"
payload.dataType shouldEqual ErrorPayload.NONE
}
"Return a `ReadOutOfBounds` error if the byte range is out of bounds" in {
val requestId = UUID.randomUUID()
val filename = "bar.bin"
val barFile = new File(testContentRoot.toFile, filename)
val contents = Array[Byte](65, 66, 67) //ABC
FileUtils.writeByteArrayToFile(barFile, contents)
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val readBytesCommand = createReadBytesCommandPacket(
requestId,
Seq(filename),
testContentRootId,
3L,
2L
)
client.send(readBytesCommand)
val readResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
readResponse
.payloadType() shouldEqual OutboundPayload.ERROR
val payload = readResponse
.payload(new Error)
.asInstanceOf[Error]
payload.code shouldEqual 1009
payload.message shouldEqual "Read is out of bounds for the file"
payload.dataType() shouldEqual ErrorPayload.READ_OOB
val data = payload
.data(new ReadOutOfBoundsError)
.asInstanceOf[ReadOutOfBoundsError]
data.fileLength() shouldEqual 3
Files.delete(barFile.toPath)
}
"Return a `NotFile` error if the provided path is not a file" in {
val requestId = UUID.randomUUID()
val client = newWsClient()
client.send(createSessionInitCmd())
client.expectFrame()
val readBytesCommand = createReadBytesCommandPacket(
requestId,
Seq(),
testContentRootId,
0L,
2L
)
client.send(readBytesCommand)
val readResponse = client
.receiveMessage[OutboundMessage]()
.getOrElse(fail("Should be right"))
readResponse
.payloadType() shouldEqual OutboundPayload.ERROR
val payload = readResponse
.payload(new Error)
.asInstanceOf[Error]
payload.code shouldEqual 1007
payload.message shouldEqual "Path is not a file"
payload.dataType shouldEqual ErrorPayload.NONE
}
}
def createChecksumBytesCommandPacket(
requestId: UUID,
pathSegments: Seq[String],
rootId: UUID,
byteOffset: Long,
length: Long
): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val path = PathFactory.create(rootId, pathSegments)
val fileSegment = FileSegmentFactory.create(path, byteOffset, length)
val command = ChecksumBytesCommandFactory.create(fileSegment)
val incomingMessage = InboundMessageFactory.create(
requestId,
None,
InboundPayload.CHECKSUM_BYTES_CMD,
command
)
builder.finish(incomingMessage)
builder.dataBuffer()
}
def createWriteBytesCommandPacket(
requestId: UUID,
pathSegments: Seq[String],
rootId: UUID,
byteOffset: Long,
bytes: Array[Byte],
overwriteExisting: Boolean
): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val path = PathFactory.create(rootId, pathSegments)
val command = WriteBytesCommandFactory.create(
path,
byteOffset,
overwriteExisting,
bytes
)
val incomingMessage = InboundMessageFactory.create(
requestId,
None,
InboundPayload.WRITE_BYTES_CMD,
command
)
builder.finish(incomingMessage)
builder.dataBuffer()
}
def createReadBytesCommandPacket(
requestId: UUID,
pathSegments: Seq[String],
rootId: UUID,
byteOffset: Long,
length: Long
): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val path = PathFactory.create(rootId, pathSegments)
val fileSegment = FileSegmentFactory.create(path, byteOffset, length)
val command = ReadBytesCommandFactory.create(fileSegment)
val incomingMessage = InboundMessageFactory.create(
requestId,
None,
InboundPayload.READ_BYTES_CMD,
command
)
builder.finish(incomingMessage)
builder.dataBuffer()
}
def createWriteFileCmdPacket(
requestId: UUID,
pathSegment: String,
rootId: UUID,
contents: Array[Byte]
): ByteBuffer = {
implicit val builder = new FlatBufferBuilder(1024)
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val path = PathFactory.create(rootId, Seq(pathSegment))
val cmd = WriteFileCommandFactory.create(path, contents)
val cmd = WriteFileCommandFactory.create(path, contents)
val inMsg = InboundMessageFactory.create(
requestId,
@ -111,6 +608,7 @@ class BinaryFileManipulationTest extends BaseBinaryServerTest with FlakySpec {
InboundPayload.WRITE_FILE_CMD,
cmd
)
builder.finish(inMsg)
builder.dataBuffer()
}
@ -120,11 +618,10 @@ class BinaryFileManipulationTest extends BaseBinaryServerTest with FlakySpec {
pathSegment: String,
rootId: UUID
): ByteBuffer = {
implicit val builder = new FlatBufferBuilder(1024)
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(1024)
val path = PathFactory.create(rootId, Seq(pathSegment))
val cmd = ReadFileCommandFactory.create(path)
val cmd = ReadFileCommandFactory.create(path)
val inMsg = InboundMessageFactory.create(
requestId,
@ -132,6 +629,7 @@ class BinaryFileManipulationTest extends BaseBinaryServerTest with FlakySpec {
InboundPayload.READ_FILE_CMD,
cmd
)
builder.finish(inMsg)
builder.dataBuffer()
}

View File

@ -0,0 +1,17 @@
package org.enso.languageserver.websocket.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.ChecksumBytesCommand
object ChecksumBytesCommandFactory {
/** Creates a new ChecksumBytesCommand.
*
* @param fileSegment the file segment to get the checksum of
* @param builder the flat buffers builder
* @return a new binary representation of a ChecksumBytesCommand.
*/
def create(fileSegment: Int)(implicit builder: FlatBufferBuilder): Int = {
ChecksumBytesCommand.createChecksumBytesCommand(builder, fileSegment)
}
}

View File

@ -0,0 +1,21 @@
package org.enso.languageserver.websocket.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.FileSegment
object FileSegmentFactory {
/** Create a new binary representation of a file segment.
*
* @param path the path to the file in which the segment exists
* @param byteOffset the start byte in the file (inclusive)
* @param segmentLength the number of bytes to read from `byteOffset`
* @param builder the flat buffers builder
* @return a new binary representation of a file segment
*/
def create(path: Int, byteOffset: Long, segmentLength: Long)(implicit
builder: FlatBufferBuilder
): Int = {
FileSegment.createFileSegment(builder, path, byteOffset, segmentLength)
}
}

View File

@ -0,0 +1,18 @@
package org.enso.languageserver.websocket.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.ReadBytesCommand
object ReadBytesCommandFactory {
/** Creates a ReadBytes command.
*
* @param fileSegment the file segment to read bytes from
* @param builder the flatbuffers builder
* @return the offset of the ReadBytesCommand
*/
def create(fileSegment: Int)(implicit builder: FlatBufferBuilder): Int = {
ReadBytesCommand.createReadBytesCommand(builder, fileSegment)
}
}

View File

@ -0,0 +1,33 @@
package org.enso.languageserver.websocket.binary.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.binary.WriteBytesCommand
object WriteBytesCommandFactory {
/** Creates a WriteBytes command.
*
* @param path the path to the file into which bytes should be written
* @param byteOffset the byte offset at which to start writing
* @param overwriteExisting whether or not existing bytes should be
* overwritten
* @param bytes the bytes to be written
* @param builder the FlatBuffers builder
* @return the offset of the WriteBytesCommand.
*/
def create(
path: Int,
byteOffset: Long,
overwriteExisting: Boolean,
bytes: Array[Byte]
)(implicit builder: FlatBufferBuilder): Int = {
val bytesOff = builder.createByteVector(bytes)
WriteBytesCommand.createWriteBytesCommand(
builder,
path,
byteOffset,
overwriteExisting,
bytesOff
)
}
}

View File

@ -26,8 +26,7 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
)
}
"File Server" must {
"Writing files" must {
"write textual content to a file" taggedAs Retry in {
val client = getInitialisedWsClient()
client.send(json"""
@ -79,10 +78,13 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
}
}
"Reading files" must {
"read a file content" in {
val client = getInitialisedWsClient()
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/write",
"id": 4,
@ -95,13 +97,15 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 4,
"result": null
}
""")
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/read",
"id": 5,
@ -113,7 +117,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 5,
"result": { "contents": "123456789" }
@ -123,7 +128,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return FileNotFoundError if a file doesn't exist" in {
val client = getInitialisedWsClient()
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/read",
"id": 6,
@ -135,7 +141,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 6,
"error" : {
@ -145,10 +152,13 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
}
}
"Creating file-system entities" must {
"create a file" in {
val client = getInitialisedWsClient()
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 7,
@ -164,7 +174,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 7,
"result": null
@ -177,7 +188,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"create a directory" in {
val client = getInitialisedWsClient()
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 7,
@ -193,7 +205,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 7,
"result": null
@ -203,11 +216,14 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
val file = Paths.get(testContentRoot.toString, "foo1", "baz").toFile
file.isDirectory shouldBe true
}
}
"File management" must {
"delete a file" in {
val client = getInitialisedWsClient()
// create a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 8,
@ -223,7 +239,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 8,
"result": null
@ -234,7 +251,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
file.isFile shouldBe true
// delete a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/delete",
"id": 9,
@ -246,7 +264,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 9,
"result": null
@ -260,7 +279,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"delete a directory" in {
val client = getInitialisedWsClient()
// create a directory
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 10,
@ -276,7 +296,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 10,
"result": null
@ -287,7 +308,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
file.isDirectory shouldBe true
// delete a directory
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/delete",
"id": 11,
@ -299,7 +321,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 11,
"result": null
@ -312,9 +335,10 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return FileNotFound when deleting nonexistent file" in {
val client = getInitialisedWsClient()
val file = Paths.get(testContentRoot.toString, "foo1", "bar.txt").toFile
val file = Paths.get(testContentRoot.toString, "foo1", "bar.txt").toFile
file.isFile shouldBe false
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/delete",
"id": 12,
@ -326,7 +350,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 12,
"error": {
@ -342,9 +367,10 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return FileNotFound when deleting nonexistent directory" in {
val client = getInitialisedWsClient()
val file = Paths.get(testContentRoot.toString, "foo1", "baz").toFile
val file = Paths.get(testContentRoot.toString, "foo1", "baz").toFile
file.isDirectory shouldBe false
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/delete",
"id": 13,
@ -356,7 +382,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 13,
"error": {
@ -373,7 +400,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"copy a file" in {
val client = getInitialisedWsClient()
// create a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 14,
@ -389,7 +417,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 14,
"result": null
@ -399,7 +428,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
from.toFile.isFile shouldBe true
// copy a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/copy",
"id": 15,
@ -415,7 +445,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 15,
"result": null
@ -429,7 +460,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"copy a directory" in {
val client = getInitialisedWsClient()
// create a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 16,
@ -445,7 +477,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 16,
"result": null
@ -455,7 +488,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
from.toFile.isFile shouldBe true
// copy a directory
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/copy",
"id": 17,
@ -471,7 +505,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 17,
"result": null
@ -484,7 +519,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return failure when copying nonexistent file" in {
val client = getInitialisedWsClient()
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/copy",
"id": 18,
@ -500,7 +536,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 18,
"error": {
@ -517,7 +554,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"move a file" in {
val client = getInitialisedWsClient()
// create a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 19,
@ -533,7 +571,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 19,
"result": null
@ -543,7 +582,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
from.toFile.isFile shouldBe true
// move a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/move",
"id": 20,
@ -559,7 +599,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 20,
"result": null
@ -574,7 +615,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"move a directory" in {
val client = getInitialisedWsClient()
// create a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 21,
@ -590,7 +632,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 21,
"result": null
@ -603,7 +646,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
from.toFile.isDirectory shouldBe true
// move a directory
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/move",
"id": 22,
@ -619,7 +663,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 22,
"result": null
@ -633,7 +678,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return failure when moving nonexistent file" in {
val client = getInitialisedWsClient()
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/move",
"id": 23,
@ -649,7 +695,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 23,
"error": {
@ -666,7 +713,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return failure when target file exists" in {
val client = getInitialisedWsClient()
// create a source file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 24,
@ -682,7 +730,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 24,
"result": null
@ -692,7 +741,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
from.toFile.isFile shouldBe true
// create a destination file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 25,
@ -708,7 +758,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 25,
"result": null
@ -718,7 +769,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
to.toFile.isFile shouldBe true
// move to existing file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/move",
"id": 26,
@ -734,7 +786,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 26,
"error": {
@ -750,10 +803,11 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"check file existence" in {
val client = getInitialisedWsClient()
val path = Paths.get(testContentRoot.toString, "nonexistent.txt")
val path = Paths.get(testContentRoot.toString, "nonexistent.txt")
path.toFile.exists shouldBe false
// check file exists
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/exists",
"id": 27,
@ -765,7 +819,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 27,
"result": {
@ -774,7 +829,9 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
}
}
"Treeing files" must {
"get a root tree" in withCleanRoot {
val client = getInitialisedWsClient()
@ -786,7 +843,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// └── b.txt
// create base/a.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 28,
@ -802,7 +860,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 28,
"result": null
@ -810,7 +869,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// create base/subdir/b.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 29,
@ -826,7 +886,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 29,
"result": null
@ -834,7 +895,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// get a tree of a root
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/tree",
"id": 30,
@ -853,7 +915,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// ├── a.txt
// └── subdir
// └── b.txt
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 30,
"result": {
@ -931,7 +994,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// └── b.txt
// create base/a.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 31,
@ -947,7 +1011,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 31,
"result": null
@ -955,7 +1020,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// create base/subdir/b.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 32,
@ -971,7 +1037,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 32,
"result": null
@ -979,7 +1046,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// get a tree of 'base'
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/tree",
"id": 33,
@ -997,7 +1065,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// ├── a.txt
// └── subdir
// └── b.txt
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 33,
"result": {
@ -1062,7 +1131,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// └── b.txt
// create base/a.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 34,
@ -1078,7 +1148,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 34,
"result": null
@ -1086,7 +1157,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// create base/subdir/b.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 35,
@ -1102,7 +1174,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 35,
"result": null
@ -1110,7 +1183,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// get a tree of 'base'
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/tree",
"id": 36,
@ -1128,7 +1202,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// base
// ├── a.txt
// └── subdir
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 36,
"result": {
@ -1172,7 +1247,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"get a subdirectory tree" in {
val client = getInitialisedWsClient()
// create base/a.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 37,
@ -1188,7 +1264,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 37,
"result": null
@ -1196,7 +1273,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// create base/subdir/b.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 38,
@ -1212,7 +1290,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 38,
"result": null
@ -1220,7 +1299,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// get a tree of 'base/subdir'
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/tree",
"id": 39,
@ -1236,7 +1316,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
//
// subdir
// └── b.txt
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 39,
"result": {
@ -1279,7 +1360,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// └── b.txt
// create base2/subdir/b.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 40,
@ -1295,7 +1377,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 40,
"result": null
@ -1304,12 +1387,13 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// create symlink base/link -> base/subdir
val symlink = Paths.get(testContentRoot.toString, "base2", "link")
val subdir = Paths.get(testContentRoot.toString, "base2", "subdir")
val subdir = Paths.get(testContentRoot.toString, "base2", "subdir")
Files.createSymbolicLink(symlink, subdir)
Files.isSymbolicLink(symlink) shouldBe true
// get a tree of 'base'
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/tree",
"id": 41,
@ -1328,7 +1412,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// └── b.txt
// └── subdir
// └── b.txt
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 41,
"result": {
@ -1400,7 +1485,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"get a directory tree with symlink outside of root" in {
val client = getInitialisedWsClient()
// create base3
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 42,
@ -1416,7 +1502,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 42,
"result": null
@ -1431,7 +1518,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
Files.isSymbolicLink(symlink) shouldBe true
// get a tree of 'base3'
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/tree",
"id": 43,
@ -1447,7 +1535,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
//
// base3
// └── link
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 43,
"result": {
@ -1480,7 +1569,9 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
}
}
"Listing directories" must {
"list a subdirectory" in {
val client = getInitialisedWsClient()
// create:
@ -1489,7 +1580,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
// └── b.txt
// create subdir/b.txt
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 44,
@ -1505,7 +1597,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 44,
"result": null
@ -1513,7 +1606,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
""")
// get a tree of subdir
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/list",
"id": 45,
@ -1526,7 +1620,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
// expect: b.txt
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 45,
"result" : {
@ -1546,11 +1641,14 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
}
}
"Getting file information" must {
"get file info" in {
val client = getInitialisedWsClient()
// create a file
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/create",
"id": 46,
@ -1566,7 +1664,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 46,
"result": null
@ -1577,7 +1676,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
// get file info
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/info",
"id": 47,
@ -1589,7 +1689,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 47,
"result" : {
@ -1616,9 +1717,10 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
"return FileNotFound when getting info of nonexistent file" in {
val client = getInitialisedWsClient()
val file = Paths.get(testContentRoot.toString, "nonexistent.txt").toFile
val file = Paths.get(testContentRoot.toString, "nonexistent.txt").toFile
file.exists shouldBe false
client.send(json"""
client.send(
json"""
{ "jsonrpc": "2.0",
"method": "file/info",
"id": 48,
@ -1630,7 +1732,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{ "jsonrpc": "2.0",
"id": 48,
"error": {
@ -1640,7 +1743,9 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
}
""")
}
}
"file/checksum" must {
"get file checksum" in {
val client = getInitialisedWsClient()
// create a file
@ -1749,5 +1854,4 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
FileUtils.cleanDirectory(testContentRoot.toFile)
test
}
}