mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 07:51:56 +03:00
Implement byte-based file operations (#1795)
This commit is contained in:
parent
0b363e3e85
commit
90c020d666
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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],
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -75,7 +75,7 @@ class ReadBinaryFileHandler(
|
||||
val packet = ErrorFactory.createGenericError(
|
||||
error.code,
|
||||
error.message,
|
||||
Some(requestId)
|
||||
maybeCorrelationId = Some(requestId)
|
||||
)
|
||||
replyTo ! packet
|
||||
cancellable.cancel()
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -78,7 +78,7 @@ class WriteBinaryFileHandler(
|
||||
val packet = ErrorFactory.createGenericError(
|
||||
error.code,
|
||||
error.message,
|
||||
Some(requestId)
|
||||
maybeCorrelationId = Some(requestId)
|
||||
)
|
||||
replyTo ! packet
|
||||
cancellable.cancel()
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user