Store Project Manager Metadata in Project Directory (#1120)

Project Manager to stores its metadata inside the project directory, 
instead of maintaining the global index. This will allow users to move 
and modify files inside the ~/enso directory.
This commit is contained in:
Dmitry Bushev 2020-09-07 12:25:14 +03:00 committed by GitHub
parent 2da720b1a9
commit e92b9d0fc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 592 additions and 224 deletions

View File

@ -296,8 +296,8 @@ val splainOptions = Seq(
// === ZIO ====================================================================
val zioVersion = "1.0.0-RC18-2"
val zioInteropCatsVersion = "2.0.0.0-RC13"
val zioVersion = "1.0.1"
val zioInteropCatsVersion = "2.1.4.0"
val zio = Seq(
"dev.zio" %% "zio" % zioVersion,
"dev.zio" %% "zio-interop-cats" % zioInteropCatsVersion

View File

@ -6,7 +6,7 @@ import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
import akka.stream.OverflowStrategy
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import akka.testkit._
import io.circe.{ACursor, Decoder, DecodingFailure, HCursor, Json}
import io.circe.parser.parse
import org.enso.jsonrpc.{ClientControllerFactory, JsonRpcServer, Protocol}
@ -104,7 +104,7 @@ abstract class JsonRpcServerTestKit
def send(json: Json): Unit = send(json.noSpaces)
def expectMessage(timeout: FiniteDuration = 3.seconds): String =
def expectMessage(timeout: FiniteDuration = 3.seconds.dilated): String =
outActor.expectMsgClass[String](timeout, classOf[String])
def expectJson(json: Json): Assertion = {

View File

@ -32,11 +32,12 @@ project-manager {
storage {
projects-root = ${user.home}/enso
projects-root=${?PROJECTS_ROOT}
project-index-path = ${project-manager.storage.projects-root}/.enso/project-index.json
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
user-projects-path = ${project-manager.storage.projects-root}/projects
tutorials-path = ${project-manager.storage.projects-root}/tutorials
tutorials-cache-path = ${project-manager.storage.projects-root}/.tutorials-cache
project-metadata-directory = ".enso"
project-metadata-file-name = "project.json"
}
timeout {

View File

@ -1,6 +1,6 @@
package org.enso.projectmanager.boot
import zio.{Has, ZEnv}
import zio.{ExitCode, Has, ZEnv}
import zio.blocking.Blocking
import zio.clock.Clock
import zio.console.Console
@ -12,9 +12,9 @@ import zio.system.System
*/
object Globals {
val FailureExitCode = 1
val FailureExitCode = ExitCode(1)
val SuccessExitCode = 0
val SuccessExitCode = ExitCode(0)
val ConfigFilename = "application.conf"

View File

@ -3,15 +3,11 @@ package org.enso.projectmanager.boot
import akka.actor.ActorSystem
import akka.stream.SystemMaterializer
import cats.MonadError
import io.circe.generic.auto._
import org.enso.jsonrpc.JsonRpcServer
import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
import org.enso.projectmanager.control.effect.{Async, ErrorChannel, Exec, Sync}
import org.enso.projectmanager.infrastructure.file.{
BlockingFileSystem,
SynchronizedFileStorage
}
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
import org.enso.projectmanager.infrastructure.languageserver.{
LanguageServerGatewayImpl,
LanguageServerRegistry,
@ -19,10 +15,7 @@ import org.enso.projectmanager.infrastructure.languageserver.{
}
import org.enso.projectmanager.infrastructure.log.Slf4jLogging
import org.enso.projectmanager.infrastructure.random.SystemGenerator
import org.enso.projectmanager.infrastructure.repository.{
ProjectFileRepository,
ProjectIndex
}
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepository
import org.enso.projectmanager.infrastructure.time.RealClock
import org.enso.projectmanager.protocol.{
JsonRpc,
@ -40,7 +33,9 @@ import scala.concurrent.ExecutionContext
/**
* A main module containing all components of the project manager.
*/
class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Async](
class MainModule[
F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Applicative: Async
](
config: ProjectManagerConfig,
computeExecutionContext: ExecutionContext
)(implicit
@ -60,22 +55,18 @@ class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Async](
lazy val fileSystem =
new BlockingFileSystem[F](config.timeout.ioTimeout)
lazy val indexStorage = new SynchronizedFileStorage[ProjectIndex, F](
config.storage.projectIndexPath,
fileSystem
)
lazy val gen = new SystemGenerator[F]
lazy val projectValidator = new MonadicProjectValidator[F]()
lazy val projectRepository =
new ProjectFileRepository[F](
config.storage,
clock,
fileSystem,
indexStorage
gen
)
lazy val gen = new SystemGenerator[F]
lazy val projectValidator = new MonadicProjectValidator[F]()
lazy val languageServerRegistry =
system.actorOf(
LanguageServerRegistry

View File

@ -99,7 +99,7 @@ object ProjectManager extends App with LazyLogging {
success = ZIO.succeed(_)
)
override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = {
override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = {
Cli.parse(args.toArray) match {
case Right(opts) =>
runOpts(opts)
@ -114,7 +114,7 @@ object ProjectManager extends App with LazyLogging {
* The main function of the application, which will be passed the command-line
* arguments to the program and has to return an `IO` with the errors fully handled.
*/
def runOpts(options: CommandLine): ZIO[ZEnv, Nothing, Int] = {
def runOpts(options: CommandLine): ZIO[ZEnv, Nothing, ExitCode] = {
if (options.hasOption(Cli.HELP_OPTION)) {
ZIO.effectTotal(Cli.printHelp()) *>
ZIO.succeed(SuccessExitCode)
@ -147,7 +147,9 @@ object ProjectManager extends App with LazyLogging {
}
}
private def displayVersion(useJson: Boolean): ZIO[Console, Nothing, Int] = {
private def displayVersion(
useJson: Boolean
): ZIO[Console, Nothing, ExitCode] = {
val versionDescription = VersionDescription.make(
"Enso Project Manager",
includeRuntimeJVMInfo = true

View File

@ -1,7 +1,7 @@
package org.enso.projectmanager.boot
import com.typesafe.scalalogging.LazyLogging
import zio.Cause
import zio.{Cause, Supervisor}
import zio.internal.stacktracer.Tracer
import zio.internal.stacktracer.impl.AkkaLineNumbersTracer
import zio.internal.tracing.TracingConfig
@ -41,4 +41,6 @@ class ZioPlatform(computeExecutionContext: ExecutionContext)
if (cause.died)
logger.error(cause.prettyPrint)
override def supervisor: Supervisor[Any] =
Supervisor.none
}

View File

@ -32,13 +32,16 @@ object configuration {
* A configuration object for properties of project storage.
*
* @param projectsRoot a project root
* @param projectIndexPath a path to the index
* @param userProjectsPath a user project root
* @param projectMetadataDirectory a directory name containing project
* metadata
* @param projectMetadataFileName a name of project metadata file
*/
case class StorageConfig(
projectsRoot: File,
projectIndexPath: File,
userProjectsPath: File
userProjectsPath: File,
projectMetadataDirectory: String,
projectMetadataFileName: String
)
/**

View File

@ -0,0 +1,43 @@
package org.enso.projectmanager.control.core
import zio.ZIO
/**
* An applicative functor.
*/
trait Applicative[F[+_, +_]] {
/**
* Lifts value into the applicative functor.
*/
def pure[E, A](a: A): F[E, A]
/**
* Applies the function in the applicative context to the given value.
*/
def ap[E, E1 >: E, A, B](ff: F[E, A => B])(fa: F[E1, A]): F[E1, B]
/**
* Applies function f to the given value.
*/
def map[E, A, B](fa: F[E, A])(f: A => B): F[E, B]
/**
* Applies the binary function to the effectful falues ma and mb.
*/
def map2[E, A, B, C](ma: F[E, A], mb: F[E, B])(f: (A, B) => C): F[E, C]
}
object Applicative {
def apply[F[+_, +_]](implicit applicative: Applicative[F]): Applicative[F] =
applicative
implicit def zioApplicative[R]: Applicative[ZIO[R, +*, +*]] =
zioApplicativeInstance.asInstanceOf[Applicative[ZIO[R, +*, +*]]]
final private[this] val zioApplicativeInstance
: Applicative[ZIO[Any, +*, +*]] =
new ZioApplicative[Any]
}

View File

@ -0,0 +1,10 @@
package org.enso.projectmanager.control.core
class ApplicativeFunctionOps[F[+_, +_]: Applicative, E, A, B](
ff: F[E, A => B]
) {
/** Alias for [[Applicative.ap]]. */
def <*>[E1 <: E](fa: F[E1, A]): F[E, B] =
Applicative[F].ap(ff)(fa)
}

View File

@ -0,0 +1,12 @@
package org.enso.projectmanager.control.core
class ApplicativeOps[F[+_, +_]: Applicative, E, A](fa: F[E, A]) {
/** Compose two actions, discarding any value produced by the first. */
def *>[E1 >: E, B](fb: F[E, B]): F[E1, B] =
Applicative[F].ap(Applicative[F].map(fa)(_ => (b: B) => b))(fb)
/** Compose two actions, discarding any value produced by the second. */
def <*[E1 >: E, B](fb: F[E1, B]): F[E1, A] =
Applicative[F].ap(Applicative[F].map(fb)(_ => (a: A) => a))(fa)
}

View File

@ -8,7 +8,7 @@ import scala.util.{Either, Left, Right}
* A class for covariant effects containing error channel used to chain
* computations.
*
* @tparam F a effectful context
* @tparam F an effectful context
*/
trait CovariantFlatMap[F[+_, +_]] {

View File

@ -0,0 +1,36 @@
package org.enso.projectmanager.control.core
/**
* A traversal over a structure with effects.
*/
trait Traverse[G[_]] {
/**
* Applies the function `f` to each element of the collection.
*
* @param s a collection of elements
* @param f the mapping function
* @return the new collection with the function applied to each element
*/
def traverse[F[+_, +_]: Applicative, E, A, B](s: G[A])(
f: A => F[E, B]
): F[E, G[B]]
}
object Traverse {
def apply[G[_]](implicit traverse: Traverse[G]): Traverse[G] =
traverse
implicit object ListTraverse extends Traverse[List] {
/** @inheritdoc */
override def traverse[F[+_, +_]: Applicative, E, A, B](
xs: List[A]
)(f: A => F[E, B]): F[E, List[B]] =
xs.foldRight(Applicative[F].pure[E, List[B]](List())) { (a, facc) =>
Applicative[F].map2(f(a), facc)(_ :: _)
}
}
}

View File

@ -0,0 +1,29 @@
package org.enso.projectmanager.control.core
import zio.ZIO
/**
* An instance of [[Applicative]] class for ZIO.
*/
private[core] class ZioApplicative[R] extends Applicative[ZIO[R, +*, +*]] {
/** @inheritdoc */
override def pure[E, A](a: A): ZIO[R, E, A] =
ZIO.succeed(a)
/** @inheritdoc */
override def ap[E, E1 >: E, A, B](
ff: ZIO[R, E, A => B]
)(fa: ZIO[R, E1, A]): ZIO[R, E1, B] =
ff.flatMap(f => fa.map(f))
/** @inheritdoc */
override def map[E, A, B](fa: ZIO[R, E, A])(f: A => B): ZIO[R, E, B] =
fa.map(f)
/** @inheritdoc */
override def map2[E, A, B, C](ma: ZIO[R, E, A], mb: ZIO[R, E, B])(
f: (A, B) => C
): ZIO[R, E, C] =
ma.flatMap(a => mb.map(b => f(a, b)))
}

View File

@ -8,10 +8,10 @@ import zio.ZIO
private[core] class ZioCovariantFlatMap[R]
extends CovariantFlatMap[ZIO[R, +*, +*]] {
/** @inheritdoc **/
/** @inheritdoc */
override def pure[A](value: A): ZIO[R, Nothing, A] = ZIO.succeed(value)
/** @inheritdoc **/
/** @inheritdoc */
override def flatMap[E1, E2 >: E1, A, B](
fa: ZIO[R, E1, A]
)(f: A => ZIO[R, E2, B]): ZIO[R, E2, B] =

View File

@ -12,4 +12,19 @@ object syntax {
fa: F[E, A]
): CovariantFlatMapOps[F, E, A] = new CovariantFlatMapOps[F, E, A](fa)
/**
* Implicit conversion to [[ApplicativeOps]].
*/
implicit def toApplicativeOps[F[+_, +_]: Applicative, E, A](
fa: F[E, A]
): ApplicativeOps[F, E, A] =
new ApplicativeOps[F, E, A](fa)
/**
* Implicit conversion to [[ApplicativeFunctionOps]].
*/
implicit def toApplicativeFunctionOps[F[+_, +_]: Applicative, E, A, B](
ff: F[E, A => B]
): ApplicativeFunctionOps[F, E, A, B] =
new ApplicativeFunctionOps[F, E, A, B](ff)
}

View File

@ -1,6 +1,10 @@
package org.enso.projectmanager.infrastructure.file
import java.io.{File, FileNotFoundException}
import java.nio.file.{AccessDeniedException, NoSuchFileException}
import java.nio.file.{
AccessDeniedException,
NoSuchFileException,
NotDirectoryException
}
import org.apache.commons.io.{FileExistsException, FileUtils}
import org.enso.projectmanager.control.effect.syntax._
@ -60,7 +64,7 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
.mapError(toFsFailure)
.timeoutFail(OperationTimeout)(ioTimeout)
/** @inheritdoc * */
/** @inheritdoc */
override def move(from: File, to: File): F[FileSystemFailure, Unit] =
Sync[F]
.blockingOp {
@ -75,14 +79,28 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
}
.mapError(toFsFailure)
/** @inheritdoc * */
/** @inheritdoc */
override def exists(file: File): F[FileSystemFailure, Boolean] =
Sync[F]
.blockingOp(file.exists())
.mapError(toFsFailure)
/** @inheritdoc */
override def list(directory: File): F[FileSystemFailure, List[File]] =
Sync[F]
.blockingOp {
val res = directory.listFiles()
if (res eq null) {
throw new NotDirectoryException(s"Not a directory: $directory")
} else {
res.toList
}
}
.mapError(toFsFailure)
private val toFsFailure: Throwable => FileSystemFailure = {
case _: FileNotFoundException => FileNotFound
case _: NotDirectoryException => NotDirectory
case _: NoSuchFileException => FileNotFound
case _: FileExistsException => FileExists
case _: AccessDeniedException => AccessDenied

View File

@ -54,4 +54,11 @@ trait FileSystem[F[+_, +_]] {
*/
def exists(file: File): F[FileSystemFailure, Boolean]
/**
* List files in the directory.
*
* @param directory a path to the directory
* @return the directory contents
*/
def list(directory: File): F[FileSystemFailure, List[File]]
}

View File

@ -17,6 +17,11 @@ object FileSystemFailure {
*/
case object FileNotFound extends FileSystemFailure
/**
* Signals that the file is not a directory.
*/
case object NotDirectory extends FileSystemFailure
/**
* Signals that the file already exists.
*/

View File

@ -8,10 +8,8 @@ import io.circe.{Decoder, Encoder}
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Semaphore, Sync}
import org.enso.projectmanager.data.Default
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.infrastructure.file.FileStorage._
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.FileNotFound
import shapeless.{:+:, CNil, _}
/**
@ -22,13 +20,14 @@ import shapeless.{:+:, CNil, _}
* @param fileSystem a filesystem algebra
* @tparam A a datatype to store
*/
class SynchronizedFileStorage[A: Encoder: Decoder: Default, F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
class JsonFileStorage[
A: Encoder: Decoder,
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
](
path: File,
fileSystem: FileSystem[F]
) extends FileStorage[A, F] {
private val semaphore = Semaphore.unsafeMake[F](1)
/**
* Loads the serialized object from the file.
*
@ -39,9 +38,6 @@ class SynchronizedFileStorage[A: Encoder: Decoder: Default, F[+_, +_]: Sync: Err
.readFile(path)
.mapError(Coproduct[LoadFailure](_))
.flatMap(tryDecodeFileContents)
.recover {
case Inr(Inl(FileNotFound)) => Default[A].value
}
private def tryDecodeFileContents(contents: String): F[LoadFailure, A] = {
decode[A](contents) match {
@ -76,13 +72,11 @@ class SynchronizedFileStorage[A: Encoder: Decoder: Default, F[+_, +_]: Sync: Err
f: A => (A, B)
): F[CannotDecodeData :+: FileSystemFailure :+: CNil, B] =
// format: off
semaphore.withPermit {
for {
index <- load()
(updated, output) = f(index)
_ <- persist(updated).mapError(Coproduct[LoadFailure](_))
} yield output
}
for {
index <- load()
(updated, output) = f(index)
_ <- persist(updated).mapError(Coproduct[LoadFailure](_))
} yield output
// format: on
}

View File

@ -0,0 +1,97 @@
package org.enso.projectmanager.infrastructure.repository
import java.io.File
import io.circe.generic.auto._
import org.enso.projectmanager.boot.configuration.StorageConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.infrastructure.file.FileStorage.{
CannotDecodeData,
LoadFailure
}
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.FileNotFound
import org.enso.projectmanager.infrastructure.file.{
FileStorage,
FileSystem,
FileSystemFailure,
JsonFileStorage
}
import org.enso.projectmanager.infrastructure.random.Generator
import org.enso.projectmanager.infrastructure.time.Clock
import org.enso.projectmanager.model.{ProjectKind, ProjectMetadata}
import shapeless.{Coproduct, Inl, Inr}
/**
* File based implementation of the project metadata storage.
*
* @param directory a project directory
* @param storageConfig a storage config
* @param clock a clock
* @param fileSystem a file system abstraction
* @param gen a random generator
*/
final class MetadataFileStorage[
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
](
directory: File,
storageConfig: StorageConfig,
clock: Clock[F],
fileSystem: FileSystem[F],
gen: Generator[F]
) extends FileStorage[ProjectMetadata, F] {
private val file = new JsonFileStorage[ProjectMetadata, F](
metadataPath(directory),
fileSystem
)
/** @inheritdoc */
override def load(): F[LoadFailure, ProjectMetadata] =
file
.load()
.recoverWith {
case Inl(CannotDecodeData(_)) =>
init.mapError(Coproduct[LoadFailure](_))
case Inr(Inl(FileNotFound)) =>
init.mapError(Coproduct[LoadFailure](_))
}
/** @inheritdoc */
override def persist(metadata: ProjectMetadata): F[FileSystemFailure, Unit] =
file.persist(metadata)
/** @inheritdoc */
override def modify[A](
f: ProjectMetadata => (ProjectMetadata, A)
): F[LoadFailure, A] =
file.modify(f)
private def init: F[FileSystemFailure, ProjectMetadata] =
for {
metadata <- freshMetadata
_ <- file.persist(metadata)
} yield metadata
private def freshMetadata: F[FileSystemFailure, ProjectMetadata] =
for {
now <- clock.nowInUtc()
projectId <- gen.randomUUID()
} yield ProjectMetadata(
id = projectId,
kind = ProjectKind.UserProject,
created = now,
lastOpened = None
)
private def metadataPath(project: File): File =
new File(
project,
new File(
storageConfig.projectMetadataDirectory,
storageConfig.projectMetadataFileName
).toString
)
}

View File

@ -5,86 +5,103 @@ import java.util.UUID
import org.enso.pkg.{Package, PackageManager}
import org.enso.projectmanager.boot.configuration.StorageConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.{
Applicative,
CovariantFlatMap,
Traverse
}
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.infrastructure.file.{FileStorage, FileSystem}
import org.enso.projectmanager.infrastructure.file.FileSystem
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.{
FileNotFound,
NotDirectory
}
import org.enso.projectmanager.infrastructure.random.Generator
import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{
InconsistentStorage,
ProjectNotFoundInIndex,
StorageFailure
}
import org.enso.projectmanager.model.Project
import org.enso.projectmanager.infrastructure.time.Clock
import org.enso.projectmanager.model.{Project, ProjectMetadata}
/**
* File based implementation of the project repository.
*
* @param storageConfig a storage config
* @param clock a clock
* @param fileSystem a file system abstraction
* @param indexStorage an index storage
* @param gen a random generator
*/
class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
class ProjectFileRepository[
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
](
storageConfig: StorageConfig,
clock: Clock[F],
fileSystem: FileSystem[F],
indexStorage: FileStorage[ProjectIndex, F]
gen: Generator[F]
) extends ProjectRepository[F] {
/** @inheritdoc * */
/** @inheritdoc */
override def exists(
name: String
): F[ProjectRepositoryFailure, Boolean] =
indexStorage
.load()
.map(_.exists(name))
.mapError(_.fold(convertFileStorageFailure))
getAll().map(_.exists(_.name == PackageManager.Default.normalizeName(name)))
/** @inheritdoc * */
/** @inheritdoc */
override def find(
predicate: Project => Boolean
): F[ProjectRepositoryFailure, List[Project]] =
indexStorage
.load()
.map(_.find(predicate))
.mapError(_.fold(convertFileStorageFailure))
getAll().map(_.filter(predicate))
/** @inheritdoc * */
/** @inheritdoc */
override def getAll(): F[ProjectRepositoryFailure, List[Project]] =
indexStorage
.load()
.map(_.projects.values.toList)
.mapError(_.fold(convertFileStorageFailure))
fileSystem
.list(storageConfig.userProjectsPath)
.recover {
case FileNotFound | NotDirectory => Nil
}
.mapError(th => StorageFailure(th.toString))
.flatMap(s => Traverse[List].traverse(s)(loadProject).map(_.flatten))
/** @inheritdoc * */
/** @inheritdoc */
override def findById(
projectId: UUID
): F[ProjectRepositoryFailure, Option[Project]] =
indexStorage
.load()
.map(_.findById(projectId))
.mapError(_.fold(convertFileStorageFailure))
getAll().map(_.find(_.id == projectId))
/** @inheritdoc * */
/** @inheritdoc */
override def create(
project: Project
): F[ProjectRepositoryFailure, Unit] =
// format: off
for {
projectPath <- findTargetPath(project)
projectWithPath = project.copy(path = Some(projectPath.toString))
_ <- createProjectStructure(project, projectPath)
_ <- update(projectWithPath)
projectPath <- findTargetPath(project)
_ <- createProjectStructure(project, projectPath)
_ <- metadataStorage(projectPath)
.persist(ProjectMetadata(project))
.mapError(th => StorageFailure(th.toString))
} yield ()
// format: on
/** @inheritdoc * */
override def update(project: Project): F[ProjectRepositoryFailure, Unit] =
indexStorage
.modify { index =>
val updated = index.upsert(project)
(updated, ())
}
.mapError(_.fold(convertFileStorageFailure))
private def loadProject(
directory: File
): F[ProjectRepositoryFailure, Option[Project]] =
for {
pkgOpt <- loadPackage(directory)
meta <- metadataStorage(directory)
.load()
.mapError(_.fold(convertFileStorageFailure))
} yield pkgOpt.map { pkg =>
Project(
id = meta.id,
name = pkg.name,
kind = meta.kind,
created = meta.created,
lastOpened = meta.lastOpened,
path = Some(directory.toString)
)
}
private def createProjectStructure(
project: Project,
@ -94,23 +111,22 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
.blockingOp { PackageManager.Default.create(projectPath, project.name) }
.mapError(th => StorageFailure(th.toString))
/** @inheritdoc * */
/** @inheritdoc */
override def rename(
projectId: UUID,
name: String
): F[ProjectRepositoryFailure, Unit] = {
updateProjectName(projectId, name) *>
updatePackageName(projectId, name)
}
private def updatePackageName(
projectId: UUID,
name: String
): F[ProjectRepositoryFailure, Unit] =
for {
project <- getProject(projectId)
_ <- changePacketName(new File(project.path.get), name)
} yield ()
findById(projectId).flatMap {
case Some(project) =>
project.path match {
case Some(directory) =>
renamePackage(new File(directory), name)
case None =>
ErrorChannel[F].fail(ProjectNotFoundInIndex)
}
case None =>
ErrorChannel[F].fail(ProjectNotFoundInIndex)
}
private def getProject(
projectId: UUID
@ -129,7 +145,26 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
} yield projectPackage.config.name
}
private def changePacketName(
private def loadPackage(
projectPath: File
): F[ProjectRepositoryFailure, Option[Package[File]]] =
Sync[F]
.blockingOp { PackageManager.Default.fromDirectory(projectPath) }
.mapError(th => StorageFailure(th.toString))
private def getPackage(
projectPath: File
): F[ProjectRepositoryFailure, Package[File]] =
loadPackage(projectPath)
.flatMap {
case None =>
ErrorChannel[F].fail(
InconsistentStorage(s"Cannot find package.yaml at $projectPath")
)
case Some(projectPackage) => CovariantFlatMap[F].pure(projectPackage)
}
private def renamePackage(
projectPath: File,
name: String
): F[ProjectRepositoryFailure, Unit] =
@ -142,80 +177,56 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
.mapError(th => StorageFailure(th.toString))
}
private def getPackage(
projectPath: File
): F[ProjectRepositoryFailure, Package[File]] =
Sync[F]
.blockingOp { PackageManager.Default.fromDirectory(projectPath) }
.mapError(th => StorageFailure(th.toString))
.flatMap {
case None =>
ErrorChannel[F].fail(
InconsistentStorage(s"Cannot find package.yaml at $projectPath")
)
case Some(projectPackage) => CovariantFlatMap[F].pure(projectPackage)
}
private def updateProjectName(
projectId: UUID,
name: String
): F[ProjectRepositoryFailure, Unit] =
indexStorage
.modify { index =>
val updated = index.update(projectId)(_.copy(name = name))
(updated, ())
}
.mapError(_.fold(convertFileStorageFailure))
/** @inheritdoc * */
override def delete(
projectId: UUID
): F[ProjectRepositoryFailure, Unit] =
indexStorage
.modify { index =>
val maybeProject = index.findById(projectId)
index.remove(projectId) -> maybeProject
}
.mapError(_.fold(convertFileStorageFailure))
.flatMap {
case None =>
ErrorChannel[F].fail(ProjectNotFoundInIndex)
case Some(project) if project.path.isEmpty =>
ErrorChannel[F].fail(
InconsistentStorage(
"Index cannot contain a user project without path"
/** @inheritdoc */
def update(project: Project): F[ProjectRepositoryFailure, Unit] =
project.path match {
case Some(path) =>
metadataStorage(new File(path))
.persist(
ProjectMetadata(
id = project.id,
kind = project.kind,
created = project.created,
lastOpened = project.lastOpened
)
)
.mapError(th => StorageFailure(th.toString))
case None =>
ErrorChannel[F].fail(ProjectNotFoundInIndex)
}
/** @inheritdoc */
override def delete(projectId: UUID): F[ProjectRepositoryFailure, Unit] = {
findById(projectId)
.flatMap {
case Some(project) =>
removeProjectStructure(project.path.get)
project.path match {
case Some(directory) =>
fileSystem
.removeDir(new File(directory))
.mapError(th => StorageFailure(th.toString))
case None =>
ErrorChannel[F].fail(ProjectNotFoundInIndex)
}
case None =>
ErrorChannel[F].fail(ProjectNotFoundInIndex)
}
}
private def removeProjectStructure(
projectPath: String
): F[ProjectRepositoryFailure, Unit] =
fileSystem
.removeDir(new File(projectPath))
.mapError[ProjectRepositoryFailure](failure =>
StorageFailure(failure.toString)
)
/** @inheritdoc * */
/** @inheritdoc */
override def moveProjectToTargetDir(
projectId: UUID
projectId: UUID,
newName: String
): F[ProjectRepositoryFailure, File] = {
def move(project: Project) =
for {
targetPath <- findTargetPath(project)
targetPath <- findTargetPath(project.copy(name = newName))
_ <- moveProjectDir(project, targetPath)
_ <- updateProjectDir(projectId, targetPath)
} yield targetPath
for {
project <- getProject(projectId)
primaryPath = getPrimaryPath(project)
primaryPath = new File(storageConfig.userProjectsPath, newName)
finalPath <-
if (isLocationOk(project.path.get, primaryPath.toString)) {
CovariantFlatMap[F].pure(primaryPath)
@ -238,17 +249,6 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
}
}
private def updateProjectDir(projectId: UUID, targetPath: File) = {
indexStorage
.modify { index =>
val updated = index.update(projectId)(
_.copy(path = Some(targetPath.toString))
)
(updated, ())
}
.mapError(_.fold(convertFileStorageFailure))
}
private def moveProjectDir(project: Project, targetPath: File) = {
fileSystem
.move(new File(project.path.get), targetPath)
@ -257,11 +257,6 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
)
}
private def getPrimaryPath(
project: Project
): File =
new File(storageConfig.userProjectsPath, project.name)
private def findTargetPath(
project: Project
): F[ProjectRepositoryFailure, File] =
@ -290,4 +285,12 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
if (number == 0) ""
else s"_$number"
private def metadataStorage(projectPath: File): MetadataFileStorage[F] =
new MetadataFileStorage[F](
projectPath,
storageConfig,
clock,
fileSystem,
gen
)
}

View File

@ -84,8 +84,12 @@ trait ProjectRepository[F[+_, +_]] {
* Moves project to the target dir.
*
* @param projectId the project id
* @param newName the new project name
*/
def moveProjectToTargetDir(projectId: UUID): F[ProjectRepositoryFailure, File]
def moveProjectToTargetDir(
projectId: UUID,
newName: String
): F[ProjectRepositoryFailure, File]
/**
* Gets a package name for the specified project.

View File

@ -0,0 +1,36 @@
package org.enso.projectmanager.model
import java.time.OffsetDateTime
import java.util.UUID
/**
* Project metadata entity.
*
* @param id a project id
* @param kind a project kind
* @param created a project creation time
* @param lastOpened a project last open time
*/
case class ProjectMetadata(
id: UUID,
kind: ProjectKind,
created: OffsetDateTime,
lastOpened: Option[OffsetDateTime]
)
object ProjectMetadata {
/**
* Create an instance from the project entity.
*
* @param project the project entity
* @return the project metadata
*/
def apply(project: Project): ProjectMetadata =
new ProjectMetadata(
id = project.id,
kind = project.kind,
created = project.created,
lastOpened = project.lastOpened
)
}

View File

@ -17,21 +17,23 @@ import org.enso.projectmanager.infrastructure.shutdown.ShutdownHook
* A hook responsible for moving a project to the target dir.
*
* @param projectId a project id
* @param newName a new project name
* @param repo a project repository
* @param log a logging facility
*/
class MoveProjectDirCmd[F[+_, +_]: CovariantFlatMap: ErrorChannel](
projectId: UUID,
newName: String,
repo: ProjectRepository[F],
log: Logging[F]
) extends ShutdownHook[F] {
/** @inheritdoc **/
/** @inheritdoc */
override def execute(): F[Nothing, Unit] = {
def go() =
for {
_ <- log.debug(s"Moving project ${projectId}")
dir <- repo.moveProjectToTargetDir(projectId)
_ <- log.debug(s"Moving project ${projectId} to $newName")
dir <- repo.moveProjectToTargetDir(projectId, newName)
_ <- log.info(s"Project $projectId moved to $dir")
} yield ()

View File

@ -59,11 +59,11 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
): F[ProjectServiceFailure, UUID] = {
// format: off
for {
_ <- log.debug(s"Creating project $name.")
projectId <- gen.randomUUID()
_ <- log.debug(s"Creating project $name $projectId.")
_ <- validateName(name)
_ <- checkIfNameExists(name)
creationTime <- clock.nowInUtc()
projectId <- gen.randomUUID()
project = Project(projectId, name, UserProject, creationTime)
_ <- repo.create(project).mapError(toServiceFailure)
_ <- log.info(s"Project $project created.")
@ -108,7 +108,7 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
_ <- checkIfNameExists(name)
oldPackage <- repo.getPackageName(projectId).mapError(toServiceFailure)
_ <- repo.rename(projectId, name).mapError(toServiceFailure)
_ <- renameProjectDirOrRegisterShutdownHook(projectId)
_ <- renameProjectDirOrRegisterShutdownHook(projectId, name)
newPackage = PackageManager.Default.normalizeName(name)
_ <- refactorProjectName(projectId, oldPackage, newPackage)
_ <- log.info(s"Project $projectId renamed.")
@ -116,9 +116,10 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
}
private def renameProjectDirOrRegisterShutdownHook(
projectId: UUID
projectId: UUID,
newName: String
): F[ProjectServiceFailure, Unit] = {
val cmd = new MoveProjectDirCmd[F](projectId, repo, log)
val cmd = new MoveProjectDirCmd[F](projectId, newName, repo, log)
CovariantFlatMap[F]
.ifM(isServerRunning(projectId))(
ifTrue = languageServerGateway.registerShutdownHook(projectId, cmd),

View File

@ -5,27 +5,21 @@ import java.nio.file.Files
import java.time.{OffsetDateTime, ZoneOffset}
import java.util.UUID
import io.circe.generic.auto._
import akka.testkit._
import org.apache.commons.io.FileUtils
import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
import org.enso.projectmanager.boot.Globals.{ConfigFilename, ConfigNamespace}
import org.enso.projectmanager.boot.configuration._
import org.enso.projectmanager.control.effect.ZioEnvExec
import org.enso.projectmanager.infrastructure.file.{
BlockingFileSystem,
SynchronizedFileStorage
}
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
import org.enso.projectmanager.infrastructure.languageserver.{
LanguageServerGatewayImpl,
LanguageServerRegistry,
ShutdownHookActivator
}
import org.enso.projectmanager.infrastructure.log.Slf4jLogging
import org.enso.projectmanager.infrastructure.repository.{
ProjectFileRepository,
ProjectIndex
}
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepository
import org.enso.projectmanager.protocol.{
JsonRpc,
ManagerClientControllerFactory
@ -37,6 +31,7 @@ import pureconfig.generic.auto._
import zio.interop.catz.core._
import zio.{Runtime, Semaphore, ZEnv, ZIO}
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
class BaseServerSpec extends JsonRpcServerTestKit {
@ -53,7 +48,9 @@ class BaseServerSpec extends JsonRpcServerTestKit {
val testClock =
new ProgrammableClock[ZEnv](OffsetDateTime.now(ZoneOffset.UTC))
def getGeneratedUUID: UUID = gen.takeFirst()
def getGeneratedUUID: UUID = {
Await.result(Future(gen.takeFirst())(system.dispatcher), 3.seconds.dilated)
}
lazy val gen = new ObservableGenerator[ZEnv]()
@ -62,12 +59,11 @@ class BaseServerSpec extends JsonRpcServerTestKit {
val userProjectDir = new File(testProjectsRoot, "projects")
val indexFile = new File(testProjectsRoot, "project-index.json")
lazy val testStorageConfig = StorageConfig(
projectsRoot = testProjectsRoot,
projectIndexPath = indexFile,
userProjectsPath = userProjectDir
projectsRoot = testProjectsRoot,
userProjectsPath = userProjectDir,
projectMetadataDirectory = ".enso",
projectMetadataFileName = "project.json"
)
lazy val bootloaderConfig = config.bootloader
@ -85,17 +81,12 @@ class BaseServerSpec extends JsonRpcServerTestKit {
lazy val storageSemaphore =
Runtime.default.unsafeRun(Semaphore.make(1))
lazy val indexStorage =
new SynchronizedFileStorage[ProjectIndex, ZIO[ZEnv, +*, +*]](
testStorageConfig.projectIndexPath,
fileSystem
)
lazy val projectRepository =
new ProjectFileRepository(
testStorageConfig,
testClock,
fileSystem,
indexStorage
gen
)
lazy val projectValidator = new MonadicProjectValidator[ZIO[ZEnv, *, *]]()

View File

@ -5,6 +5,7 @@ import java.nio.file.Paths
import java.util.UUID
import io.circe.literal._
import org.apache.commons.io.FileUtils
import org.enso.projectmanager.test.Net.tryConnect
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.FlakySpec
@ -16,6 +17,11 @@ class ProjectManagementApiSpec
with FlakySpec
with ProjectManagementOps {
override def beforeEach(): Unit = {
super.beforeEach()
gen.reset()
}
"project/create" must {
"check if project name is not empty" taggedAs Flaky in {
@ -110,9 +116,11 @@ class ProjectManagementApiSpec
val projectDir = new File(userProjectDir, projectName)
val packageFile = new File(projectDir, "package.yaml")
val mainEnso = Paths.get(projectDir.toString, "src", "Main.enso").toFile
val meta = Paths.get(projectDir.toString, ".enso", "project.json").toFile
packageFile shouldBe Symbol("file")
mainEnso shouldBe Symbol("file")
meta shouldBe Symbol("file")
}
"create a project dir with a suffix if a directory is taken" in {
@ -313,6 +321,58 @@ class ProjectManagementApiSpec
deleteProject(projectId)(client1)
}
"start the Language Server after moving the directory" taggedAs Flaky in {
//given
val projectName = "foo"
implicit val client = new WsTestClient(address)
val projectId = createProject(projectName)
val newName = "bar"
val newProjectDir = new File(userProjectDir, newName)
FileUtils.moveDirectory(
new File(userProjectDir, projectName),
newProjectDir
)
val packageFile = new File(newProjectDir, "package.yaml")
val mainEnso =
Paths.get(newProjectDir.toString, "src", "Main.enso").toFile
val meta =
Paths.get(newProjectDir.toString, ".enso", "project.json").toFile
packageFile shouldBe Symbol("file")
mainEnso shouldBe Symbol("file")
meta shouldBe Symbol("file")
//when
val socket = openProject(projectId)
val languageServerClient =
new WsTestClient(s"ws://${socket.host}:${socket.port}")
languageServerClient.send(json"""
{
"jsonrpc": "2.0",
"method": "file/read",
"id": 1,
"params": {
"path": {
"rootId": ${UUID.randomUUID()},
"segments": ["src", "Main.enso"]
}
}
}
""")
//then
// 'not initialized' response indicates that language server is running
languageServerClient.expectJson(json"""
{
"jsonrpc":"2.0",
"id":1,
"error":{"code":6001,"message":"Session not initialised"}}
""")
//teardown
closeProject(projectId)
deleteProject(projectId)
}
}
"project/close" must {
@ -396,9 +456,9 @@ class ProjectManagementApiSpec
"id":0,
"result": {
"projects": [
{"name": "baz", "id": $bazId, "lastOpened": null},
{"name": "bar", "id": $barId, "lastOpened": null},
{"name": "foo", "id": $fooId, "lastOpened": null}
{"name": "Baz", "id": $bazId, "lastOpened": null},
{"name": "Bar", "id": $barId, "lastOpened": null},
{"name": "Foo", "id": $fooId, "lastOpened": null}
]
}
}
@ -438,9 +498,9 @@ class ProjectManagementApiSpec
"id":0,
"result": {
"projects": [
{"name": "bar", "id": $barId, "lastOpened": $barOpenTime},
{"name": "foo", "id": $fooId, "lastOpened": $fooOpenTime},
{"name": "baz", "id": $bazId, "lastOpened": null}
{"name": "Bar", "id": $barId, "lastOpened": $barOpenTime},
{"name": "Foo", "id": $fooId, "lastOpened": $fooOpenTime},
{"name": "Baz", "id": $bazId, "lastOpened": null}
]
}
}

View File

@ -30,4 +30,10 @@ class ObservableGenerator[R] extends Generator[ZIO[R, +*, +*]] {
}
}
def reset(): Unit = {
this.synchronized {
buffer = Vector()
}
}
}