mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 21:27:15 +03:00
Add project/duplicate endpoint (#10407)
close #10367 Changelog: - add: `project/duplicate` project manager command to duplicate existing projects
This commit is contained in:
parent
69b5f719e8
commit
764259f36d
@ -42,6 +42,7 @@ transport formats, please look [here](./protocol-architecture.md).
|
|||||||
- [`project/delete`](#projectdelete)
|
- [`project/delete`](#projectdelete)
|
||||||
- [`project/listSample`](#projectlistsample)
|
- [`project/listSample`](#projectlistsample)
|
||||||
- [`project/status`](#projectstatus)
|
- [`project/status`](#projectstatus)
|
||||||
|
- [`project/duplicate`](#projectduplicate)
|
||||||
- [Action Progress Reporting](#action-progress-reporting)
|
- [Action Progress Reporting](#action-progress-reporting)
|
||||||
- [`task/started`](#taskstarted)
|
- [`task/started`](#taskstarted)
|
||||||
- [`task/progress-update`](#taskprogress-update)
|
- [`task/progress-update`](#taskprogress-update)
|
||||||
@ -750,6 +751,50 @@ interface ProjectStatusResponse {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `project/duplicate`
|
||||||
|
|
||||||
|
This message requests to make a copy of the project.
|
||||||
|
|
||||||
|
- **Type:** Request
|
||||||
|
- **Direction:** Client -> Server
|
||||||
|
- **Connection:** Protocol
|
||||||
|
- **Visibility:** Public
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ProjectDuplicateRequest {
|
||||||
|
/**
|
||||||
|
* The project to duplicate.
|
||||||
|
*/
|
||||||
|
projectId: UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom directory with the user projects.
|
||||||
|
*/
|
||||||
|
projectsDirectory?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ProjectDuplicateResponse {
|
||||||
|
projectId: UUID;
|
||||||
|
projectName: string;
|
||||||
|
projectNormalizedName: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Errors
|
||||||
|
|
||||||
|
- [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with
|
||||||
|
underlying data store.
|
||||||
|
- [`ProjectNotFoundError`](#projectnotfounderror) to signal that the project
|
||||||
|
doesn't exist.
|
||||||
|
- [`ServiceError`](./protocol-common.md#serviceerror) to signal that the the
|
||||||
|
operation timed out.
|
||||||
|
|
||||||
## Action Progress Reporting
|
## Action Progress Reporting
|
||||||
|
|
||||||
Some actions, especially those related to installation of new components may
|
Some actions, especially those related to installation of new components may
|
||||||
|
@ -59,7 +59,7 @@ abstract class JsonRpcServerTestKit
|
|||||||
|
|
||||||
def clientControllerFactory(): ClientControllerFactory
|
def clientControllerFactory(): ClientControllerFactory
|
||||||
|
|
||||||
var _clientControllerFactory: ClientControllerFactory = _
|
private var _clientControllerFactory: ClientControllerFactory = _
|
||||||
|
|
||||||
override def beforeEach(): Unit = {
|
override def beforeEach(): Unit = {
|
||||||
super.beforeEach()
|
super.beforeEach()
|
||||||
@ -67,7 +67,7 @@ abstract class JsonRpcServerTestKit
|
|||||||
factory.init()
|
factory.init()
|
||||||
_clientControllerFactory = clientControllerFactory()
|
_clientControllerFactory = clientControllerFactory()
|
||||||
server = new JsonRpcServer(factory, _clientControllerFactory)
|
server = new JsonRpcServer(factory, _clientControllerFactory)
|
||||||
binding = Await.result(server.bind(interface, port = 0), 3.seconds)
|
binding = Await.result(server.bind(interface, port = 0), 5.seconds.dilated)
|
||||||
address = s"ws://$interface:${binding.localAddress.getPort}"
|
address = s"ws://$interface:${binding.localAddress.getPort}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,10 +174,12 @@ abstract class JsonRpcServerTestKit
|
|||||||
def fuzzyExpectJson(
|
def fuzzyExpectJson(
|
||||||
json: Json,
|
json: Json,
|
||||||
timeout: FiniteDuration = 5.seconds.dilated
|
timeout: FiniteDuration = 5.seconds.dilated
|
||||||
)(implicit pos: Position): Assertion = {
|
)(implicit pos: Position): Json = {
|
||||||
val parsed = parse(expectMessage(timeout))
|
val parsed = parse(expectMessage(timeout))
|
||||||
|
|
||||||
parsed should fuzzyMatchJson(json)
|
parsed should fuzzyMatchJson(json)
|
||||||
|
|
||||||
|
inside(parsed) { case Right(json) => json }
|
||||||
}
|
}
|
||||||
|
|
||||||
def expectNoMessage(): Unit = outActor.expectNoMessage()
|
def expectNoMessage(): Unit = outActor.expectNoMessage()
|
||||||
@ -191,9 +193,10 @@ abstract class JsonRpcServerTestKit
|
|||||||
trait FuzzyJsonMatchers { self: Matchers =>
|
trait FuzzyJsonMatchers { self: Matchers =>
|
||||||
class JsonEquals(expected: Json)
|
class JsonEquals(expected: Json)
|
||||||
extends Matcher[Either[io.circe.ParsingFailure, Json]] {
|
extends Matcher[Either[io.circe.ParsingFailure, Json]] {
|
||||||
val patch = inferPatch(expected)
|
|
||||||
|
|
||||||
def apply(left: Either[io.circe.ParsingFailure, Json]) = {
|
private val patch = inferPatch(expected)
|
||||||
|
|
||||||
|
def apply(left: Either[io.circe.ParsingFailure, Json]): MatchResult = {
|
||||||
val leftFormatted = patch[scala.util.Try](left.getOrElse(Json.Null))
|
val leftFormatted = patch[scala.util.Try](left.getOrElse(Json.Null))
|
||||||
val expectedFormatted = patch[scala.util.Try](expected)
|
val expectedFormatted = patch[scala.util.Try](expected)
|
||||||
MatchResult(
|
MatchResult(
|
||||||
|
@ -89,6 +89,20 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
|
|||||||
}
|
}
|
||||||
.mapError(toFsFailure)
|
.mapError(toFsFailure)
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
override def copy(from: File, to: File): F[FileSystemFailure, Unit] =
|
||||||
|
Sync[F]
|
||||||
|
.blockingOp {
|
||||||
|
if (to.isDirectory) {
|
||||||
|
FileUtils.copyToDirectory(from, to)
|
||||||
|
} else if (from.isDirectory) {
|
||||||
|
FileUtils.copyDirectory(from, to)
|
||||||
|
} else {
|
||||||
|
FileUtils.copyFile(from, to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mapError(toFsFailure)
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
override def exists(file: File): F[FileSystemFailure, Boolean] =
|
override def exists(file: File): F[FileSystemFailure, Boolean] =
|
||||||
Sync[F]
|
Sync[F]
|
||||||
|
@ -48,7 +48,7 @@ trait FileSystem[F[+_, +_]] {
|
|||||||
*/
|
*/
|
||||||
def remove(path: File): F[FileSystemFailure, Unit]
|
def remove(path: File): F[FileSystemFailure, Unit]
|
||||||
|
|
||||||
/** Move a file or directory recursively
|
/** Move a file or directory recursively.
|
||||||
*
|
*
|
||||||
* @param from a path to the source
|
* @param from a path to the source
|
||||||
* @param to a path to the destination
|
* @param to a path to the destination
|
||||||
@ -56,6 +56,14 @@ trait FileSystem[F[+_, +_]] {
|
|||||||
*/
|
*/
|
||||||
def move(from: File, to: File): F[FileSystemFailure, Unit]
|
def move(from: File, to: File): F[FileSystemFailure, Unit]
|
||||||
|
|
||||||
|
/** Copy a file or directory recursively.
|
||||||
|
*
|
||||||
|
* @param from a path to the source
|
||||||
|
* @param to a path to the destination
|
||||||
|
* @return either [[FileSystemFailure]] or Unit
|
||||||
|
*/
|
||||||
|
def copy(from: File, to: File): F[FileSystemFailure, Unit]
|
||||||
|
|
||||||
/** Tests if a file exists.
|
/** Tests if a file exists.
|
||||||
*
|
*
|
||||||
* @param file the file to check
|
* @param file the file to check
|
||||||
|
@ -5,6 +5,7 @@ import java.nio.file.Path
|
|||||||
import java.nio.file.attribute.FileTime
|
import java.nio.file.attribute.FileTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.enso.pkg.{Package, PackageManager}
|
import org.enso.pkg.{Package, PackageManager}
|
||||||
|
import org.enso.pkg.validation.NameValidation
|
||||||
import org.enso.projectmanager.boot.configuration.MetadataStorageConfig
|
import org.enso.projectmanager.boot.configuration.MetadataStorageConfig
|
||||||
import org.enso.projectmanager.control.core.{
|
import org.enso.projectmanager.control.core.{
|
||||||
Applicative,
|
Applicative,
|
||||||
@ -207,14 +208,7 @@ class ProjectFileRepository[
|
|||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
def update(project: Project): F[ProjectRepositoryFailure, Unit] =
|
def update(project: Project): F[ProjectRepositoryFailure, Unit] =
|
||||||
metadataStorage(project.path)
|
metadataStorage(project.path)
|
||||||
.persist(
|
.persist(ProjectMetadata(project))
|
||||||
ProjectMetadata(
|
|
||||||
id = project.id,
|
|
||||||
kind = project.kind,
|
|
||||||
created = project.created,
|
|
||||||
lastOpened = project.lastOpened
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.mapError(th => StorageFailure(th.toString))
|
.mapError(th => StorageFailure(th.toString))
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
@ -231,46 +225,55 @@ class ProjectFileRepository[
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
override def moveProjectToTargetDir(
|
override def moveProject(
|
||||||
projectId: UUID,
|
projectId: UUID,
|
||||||
newName: String
|
newName: String
|
||||||
): F[ProjectRepositoryFailure, File] = {
|
): F[ProjectRepositoryFailure, File] = {
|
||||||
def move(project: Project) =
|
def move(project: Project) =
|
||||||
for {
|
for {
|
||||||
targetPath <- findTargetPath(newName)
|
targetPath <- findTargetPath(NameValidation.normalizeName(newName))
|
||||||
_ <- moveProjectDir(project, targetPath)
|
_ <- moveProjectDir(project.path, targetPath)
|
||||||
} yield targetPath
|
} yield targetPath
|
||||||
|
|
||||||
for {
|
for {
|
||||||
project <- getProject(projectId)
|
project <- getProject(projectId)
|
||||||
primaryPath = new File(projectsPath, newName)
|
projectPath <- move(project)
|
||||||
finalPath <-
|
} yield projectPath
|
||||||
if (isLocationOk(project.path, primaryPath)) {
|
|
||||||
CovariantFlatMap[F].pure(primaryPath)
|
|
||||||
} else {
|
|
||||||
move(project)
|
|
||||||
}
|
|
||||||
} yield finalPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def isLocationOk(
|
/** @inheritdoc */
|
||||||
currentFile: File,
|
override def copyProject(
|
||||||
primaryFile: File
|
project: Project,
|
||||||
): Boolean = {
|
newName: String,
|
||||||
val currentPath = currentFile.toString
|
newMetadata: ProjectMetadata
|
||||||
val primaryPath = primaryFile.toString
|
): F[ProjectRepositoryFailure, Project] = {
|
||||||
if (currentPath.startsWith(primaryPath)) {
|
def copy(project: Project) =
|
||||||
val suffixPattern = "_\\d+"
|
for {
|
||||||
val suffix = currentPath.substring(primaryPath.length, currentPath.length)
|
targetPath <- findTargetPath(NameValidation.normalizeName(newName))
|
||||||
suffix.matches(suffixPattern)
|
_ <- copyProjectDir(project.path, targetPath)
|
||||||
} else {
|
} yield targetPath
|
||||||
false
|
|
||||||
}
|
for {
|
||||||
|
newProjectPath <- copy(project)
|
||||||
|
_ <- metadataStorage(newProjectPath)
|
||||||
|
.persist(newMetadata)
|
||||||
|
.mapError(th => StorageFailure(th.toString))
|
||||||
|
_ <- renamePackage(newProjectPath, newName)
|
||||||
|
newProject <- getProject(newMetadata.id)
|
||||||
|
} yield newProject
|
||||||
}
|
}
|
||||||
|
|
||||||
private def moveProjectDir(project: Project, targetPath: File) = {
|
private def moveProjectDir(projectPath: File, targetPath: File) = {
|
||||||
fileSystem
|
fileSystem
|
||||||
.move(project.path, targetPath)
|
.move(projectPath, targetPath)
|
||||||
|
.mapError[ProjectRepositoryFailure](failure =>
|
||||||
|
StorageFailure(failure.toString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def copyProjectDir(projectPath: File, targetPath: File) = {
|
||||||
|
fileSystem
|
||||||
|
.copy(projectPath, targetPath)
|
||||||
.mapError[ProjectRepositoryFailure](failure =>
|
.mapError[ProjectRepositoryFailure](failure =>
|
||||||
StorageFailure(failure.toString)
|
StorageFailure(failure.toString)
|
||||||
)
|
)
|
||||||
|
@ -3,8 +3,7 @@ package org.enso.projectmanager.infrastructure.repository
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
||||||
import org.enso.projectmanager.model.Project
|
|
||||||
|
|
||||||
/** An abstraction for accessing project domain objects from durable storage.
|
/** An abstraction for accessing project domain objects from durable storage.
|
||||||
*
|
*
|
||||||
@ -82,11 +81,23 @@ trait ProjectRepository[F[+_, +_]] {
|
|||||||
* @param projectId the project id
|
* @param projectId the project id
|
||||||
* @param newName the new project name
|
* @param newName the new project name
|
||||||
*/
|
*/
|
||||||
def moveProjectToTargetDir(
|
def moveProject(
|
||||||
projectId: UUID,
|
projectId: UUID,
|
||||||
newName: String
|
newName: String
|
||||||
): F[ProjectRepositoryFailure, File]
|
): F[ProjectRepositoryFailure, File]
|
||||||
|
|
||||||
|
/** Create a copy of the project.
|
||||||
|
*
|
||||||
|
* @param project the project to copy
|
||||||
|
* @param newName the new project name
|
||||||
|
* @param newMetadata the new project metadata
|
||||||
|
*/
|
||||||
|
def copyProject(
|
||||||
|
project: Project,
|
||||||
|
newName: String,
|
||||||
|
newMetadata: ProjectMetadata
|
||||||
|
): F[ProjectRepositoryFailure, Project]
|
||||||
|
|
||||||
/** Gets a package name for the specified project.
|
/** Gets a package name for the specified project.
|
||||||
*
|
*
|
||||||
* @param projectId the project id
|
* @param projectId the project id
|
||||||
|
@ -86,6 +86,11 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel: Sync](
|
|||||||
),
|
),
|
||||||
ProjectRename -> ProjectRenameHandler
|
ProjectRename -> ProjectRenameHandler
|
||||||
.props[F](projectService, timeoutConfig.requestTimeout),
|
.props[F](projectService, timeoutConfig.requestTimeout),
|
||||||
|
ProjectDuplicate -> ProjectDuplicateHandler.props[F](
|
||||||
|
projectService,
|
||||||
|
timeoutConfig.requestTimeout,
|
||||||
|
timeoutConfig.retries
|
||||||
|
),
|
||||||
EngineListInstalled -> EngineListInstalledHandler.props(
|
EngineListInstalled -> EngineListInstalledHandler.props(
|
||||||
runtimeVersionManagementService,
|
runtimeVersionManagementService,
|
||||||
timeoutConfig.requestTimeout
|
timeoutConfig.requestTimeout
|
||||||
|
@ -26,6 +26,7 @@ object JsonRpc {
|
|||||||
.registerRequest(ProjectClose)
|
.registerRequest(ProjectClose)
|
||||||
.registerRequest(ProjectRename)
|
.registerRequest(ProjectRename)
|
||||||
.registerRequest(ProjectList)
|
.registerRequest(ProjectList)
|
||||||
|
.registerRequest(ProjectDuplicate)
|
||||||
.registerNotification(TaskStarted)
|
.registerNotification(TaskStarted)
|
||||||
.registerNotification(TaskProgressUpdate)
|
.registerNotification(TaskProgressUpdate)
|
||||||
.registerNotification(TaskFinished)
|
.registerNotification(TaskFinished)
|
||||||
|
@ -83,6 +83,27 @@ object ProjectManagementApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case object ProjectDuplicate extends Method("project/duplicate") {
|
||||||
|
|
||||||
|
case class Params(projectId: UUID, projectsDirectory: Option[String])
|
||||||
|
|
||||||
|
case class Result(
|
||||||
|
projectId: UUID,
|
||||||
|
projectName: String,
|
||||||
|
projectNormalizedName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val hasParams: HasParams.Aux[this.type, ProjectDuplicate.Params] =
|
||||||
|
new HasParams[this.type] {
|
||||||
|
type Params = ProjectDuplicate.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit val hasResult: HasResult.Aux[this.type, ProjectDuplicate.Result] =
|
||||||
|
new HasResult[this.type] {
|
||||||
|
type Result = ProjectDuplicate.Result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case object ProjectOpen extends Method("project/open") {
|
case object ProjectOpen extends Method("project/open") {
|
||||||
|
|
||||||
case class Params(
|
case class Params(
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
package org.enso.projectmanager.requesthandler
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||||
|
import org.enso.projectmanager.control.core.syntax._
|
||||||
|
import org.enso.projectmanager.control.effect.{Exec, Sync}
|
||||||
|
import org.enso.projectmanager.infrastructure.file.Files
|
||||||
|
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectDuplicate
|
||||||
|
import org.enso.projectmanager.service.{
|
||||||
|
ProjectServiceApi,
|
||||||
|
ProjectServiceFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
|
||||||
|
/** A request handler for `project/duplicate` commands.
|
||||||
|
*
|
||||||
|
* @param projectService a project service
|
||||||
|
* @param requestTimeout a request timeout
|
||||||
|
* @param timeoutRetries a number of timeouts to wait until a failure is reported
|
||||||
|
*/
|
||||||
|
class ProjectDuplicateHandler[
|
||||||
|
F[+_, +_]: Exec: CovariantFlatMap: Sync
|
||||||
|
](
|
||||||
|
projectService: ProjectServiceApi[F],
|
||||||
|
requestTimeout: FiniteDuration,
|
||||||
|
timeoutRetries: Int
|
||||||
|
) extends RequestHandler[
|
||||||
|
F,
|
||||||
|
ProjectServiceFailure,
|
||||||
|
ProjectDuplicate.type,
|
||||||
|
ProjectDuplicate.Params,
|
||||||
|
ProjectDuplicate.Result
|
||||||
|
](
|
||||||
|
ProjectDuplicate,
|
||||||
|
Some(requestTimeout),
|
||||||
|
timeoutRetries
|
||||||
|
) {
|
||||||
|
|
||||||
|
override def handleRequest: ProjectDuplicate.Params => F[
|
||||||
|
ProjectServiceFailure,
|
||||||
|
ProjectDuplicate.Result
|
||||||
|
] = { params =>
|
||||||
|
for {
|
||||||
|
projectsDirectory <- Sync[F].effect(
|
||||||
|
params.projectsDirectory.map(Files.getAbsoluteFile)
|
||||||
|
)
|
||||||
|
project <- projectService.duplicateUserProject(
|
||||||
|
projectId = params.projectId,
|
||||||
|
projectsDirectory = projectsDirectory
|
||||||
|
)
|
||||||
|
_ = logger.trace(
|
||||||
|
"Duplicated project [{}] with the new name [{}].",
|
||||||
|
params.projectId,
|
||||||
|
project.name
|
||||||
|
)
|
||||||
|
} yield ProjectDuplicate.Result(project.id, project.name, project.module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ProjectDuplicateHandler {
|
||||||
|
|
||||||
|
/** Creates a configuration object used to create a [[ProjectDuplicateHandler]].
|
||||||
|
*
|
||||||
|
* @param projectService a project service
|
||||||
|
* @param requestTimeout a request timeout
|
||||||
|
* @param timeoutRetries a number of timeouts to wait until a failure is reported
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props[F[+_, +_]: Exec: CovariantFlatMap: Sync](
|
||||||
|
projectService: ProjectServiceApi[F],
|
||||||
|
requestTimeout: FiniteDuration,
|
||||||
|
timeoutRetries: Int
|
||||||
|
): Props =
|
||||||
|
Props(
|
||||||
|
new ProjectDuplicateHandler(
|
||||||
|
projectService,
|
||||||
|
requestTimeout,
|
||||||
|
timeoutRetries
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -32,7 +32,7 @@ class MoveProjectDirCmd[F[+_, +_]: CovariantFlatMap: ErrorChannel](
|
|||||||
def go() =
|
def go() =
|
||||||
for {
|
for {
|
||||||
_ <- log.debug("Moving project [{}] to [{}].", projectId, newName)
|
_ <- log.debug("Moving project [{}] to [{}].", projectId, newName)
|
||||||
dir <- repo.moveProjectToTargetDir(projectId, newName)
|
dir <- repo.moveProject(projectId, newName)
|
||||||
_ <- log.info("Project [{}] moved to [{}].", projectId, dir)
|
_ <- log.info("Project [{}] moved to [{}].", projectId, dir)
|
||||||
} yield ()
|
} yield ()
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import org.enso.projectmanager.infrastructure.repository.{
|
|||||||
ProjectRepositoryFailure
|
ProjectRepositoryFailure
|
||||||
}
|
}
|
||||||
import org.enso.projectmanager.infrastructure.time.Clock
|
import org.enso.projectmanager.infrastructure.time.Clock
|
||||||
|
import org.enso.projectmanager.model
|
||||||
import org.enso.projectmanager.model.Project
|
import org.enso.projectmanager.model.Project
|
||||||
import org.enso.projectmanager.model.ProjectKinds.UserProject
|
import org.enso.projectmanager.model.ProjectKinds.UserProject
|
||||||
import org.enso.projectmanager.service.ProjectServiceFailure._
|
import org.enso.projectmanager.service.ProjectServiceFailure._
|
||||||
@ -96,7 +97,7 @@ class ProjectService[
|
|||||||
projectsDirectory
|
projectsDirectory
|
||||||
)
|
)
|
||||||
repo = projectRepositoryFactory.getProjectRepository(projectsDirectory)
|
repo = projectRepositoryFactory.getProjectRepository(projectsDirectory)
|
||||||
name <- getNameForNewProject(projectName, projectTemplate, repo)
|
name <- getNameForNewProject(projectName, repo)
|
||||||
_ <- log.info("Created project with actual name [{}].", name)
|
_ <- log.info("Created project with actual name [{}].", name)
|
||||||
_ <- validateProjectName(name)
|
_ <- validateProjectName(name)
|
||||||
_ <- checkIfNameExists(name, repo)
|
_ <- checkIfNameExists(name, repo)
|
||||||
@ -373,6 +374,33 @@ class ProjectService[
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
override def duplicateUserProject(
|
||||||
|
projectId: UUID,
|
||||||
|
projectsDirectory: Option[File]
|
||||||
|
): F[ProjectServiceFailure, Project] =
|
||||||
|
for {
|
||||||
|
_ <- log.debug("Duplicating project [{}].", projectId)
|
||||||
|
repo = projectRepositoryFactory.getProjectRepository(projectsDirectory)
|
||||||
|
project <- getUserProject(projectId, repo)
|
||||||
|
suggestedProjectName = getNameForDuplicatedProject(project.name)
|
||||||
|
newName <- getNameForNewProject(suggestedProjectName, repo)
|
||||||
|
_ <- validateProjectName(newName)
|
||||||
|
_ <- log.debug("Validated new project name [{}]", newName)
|
||||||
|
repo = projectRepositoryFactory.getProjectRepository(projectsDirectory)
|
||||||
|
createdTime <- clock.nowInUtc()
|
||||||
|
newMetadata = model.ProjectMetadata(
|
||||||
|
id = UUID.randomUUID(),
|
||||||
|
kind = project.kind,
|
||||||
|
created = createdTime,
|
||||||
|
lastOpened = None
|
||||||
|
)
|
||||||
|
newProject <- repo
|
||||||
|
.copyProject(project, newName, newMetadata)
|
||||||
|
.mapError(toServiceFailure)
|
||||||
|
_ <- log.info("Project copied [{}].", newProject)
|
||||||
|
} yield newProject
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
override def listProjects(
|
override def listProjects(
|
||||||
maybeSize: Option[Int]
|
maybeSize: Option[Int]
|
||||||
@ -474,7 +502,6 @@ class ProjectService[
|
|||||||
|
|
||||||
private def getNameForNewProject(
|
private def getNameForNewProject(
|
||||||
projectName: String,
|
projectName: String,
|
||||||
projectTemplate: Option[String],
|
|
||||||
projectRepository: ProjectRepository[F]
|
projectRepository: ProjectRepository[F]
|
||||||
): F[ProjectServiceFailure, String] = {
|
): F[ProjectServiceFailure, String] = {
|
||||||
def mkName(name: String, suffix: Int): String =
|
def mkName(name: String, suffix: Int): String =
|
||||||
@ -490,19 +517,16 @@ class ProjectService[
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
projectTemplate match {
|
|
||||||
case Some(_) =>
|
|
||||||
CovariantFlatMap[F]
|
CovariantFlatMap[F]
|
||||||
.ifM(projectRepository.exists(projectName))(
|
.ifM(projectRepository.exists(projectName))(
|
||||||
ifTrue = findAvailableName(projectName, 1),
|
ifTrue = findAvailableName(projectName, 1),
|
||||||
ifFalse = CovariantFlatMap[F].pure(projectName)
|
ifFalse = CovariantFlatMap[F].pure(projectName)
|
||||||
)
|
)
|
||||||
.mapError(toServiceFailure)
|
.mapError(toServiceFailure)
|
||||||
case None =>
|
|
||||||
CovariantFlatMap[F].pure(projectName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private def getNameForDuplicatedProject(projectName: String): String =
|
||||||
|
s"$projectName (copy)"
|
||||||
|
|
||||||
/** Retrieve project info.
|
/** Retrieve project info.
|
||||||
*
|
*
|
||||||
|
@ -97,6 +97,17 @@ trait ProjectServiceApi[F[+_, +_]] {
|
|||||||
projectId: UUID
|
projectId: UUID
|
||||||
): F[ProjectServiceFailure, Unit]
|
): F[ProjectServiceFailure, Unit]
|
||||||
|
|
||||||
|
/** Duplicate an existing project.
|
||||||
|
*
|
||||||
|
* @param projectId the project to copy
|
||||||
|
* @param projectsDirectory the path to the projects directory
|
||||||
|
* @return the new duplicated project
|
||||||
|
*/
|
||||||
|
def duplicateUserProject(
|
||||||
|
projectId: UUID,
|
||||||
|
projectsDirectory: Option[File]
|
||||||
|
): F[ProjectServiceFailure, Project]
|
||||||
|
|
||||||
/** Lists the user's most recently opened projects..
|
/** Lists the user's most recently opened projects..
|
||||||
*
|
*
|
||||||
* @param maybeSize the size of result set
|
* @param maybeSize the size of result set
|
||||||
|
@ -56,6 +56,12 @@ class FileSystemService[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel](
|
|||||||
.move(from, to)
|
.move(from, to)
|
||||||
.mapError(_ => FileSystemServiceFailure.FileSystem("Failed to move path"))
|
.mapError(_ => FileSystemServiceFailure.FileSystem("Failed to move path"))
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
override def copy(from: File, to: File): F[FileSystemServiceFailure, Unit] =
|
||||||
|
fileSystem
|
||||||
|
.copy(from, to)
|
||||||
|
.mapError(_ => FileSystemServiceFailure.FileSystem("Failed to copy path"))
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
override def write(
|
override def write(
|
||||||
path: File,
|
path: File,
|
||||||
|
@ -30,6 +30,13 @@ trait FileSystemServiceApi[F[+_, +_]] {
|
|||||||
*/
|
*/
|
||||||
def move(from: File, to: File): F[FileSystemServiceFailure, Unit]
|
def move(from: File, to: File): F[FileSystemServiceFailure, Unit]
|
||||||
|
|
||||||
|
/** Copy a file or directory recursively.
|
||||||
|
*
|
||||||
|
* @param from the target path
|
||||||
|
* @param to the destination path
|
||||||
|
*/
|
||||||
|
def copy(from: File, to: File): F[FileSystemServiceFailure, Unit]
|
||||||
|
|
||||||
/** Writes a file
|
/** Writes a file
|
||||||
*
|
*
|
||||||
* @param path the file path to write
|
* @param path the file path to write
|
||||||
|
@ -116,53 +116,6 @@ class ProjectManagementApiSpec
|
|||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
"fail when the project with the same name exists" in {
|
|
||||||
implicit val client: WsTestClient = new WsTestClient(address)
|
|
||||||
client.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "project/create",
|
|
||||||
"id": 1,
|
|
||||||
"params": {
|
|
||||||
"name": "Foo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
val projectId = getGeneratedUUID
|
|
||||||
client.expectJson(json"""
|
|
||||||
{
|
|
||||||
"jsonrpc" : "2.0",
|
|
||||||
"id" : 1,
|
|
||||||
"result" : {
|
|
||||||
"projectId" : $projectId,
|
|
||||||
"projectName" : "Foo",
|
|
||||||
"projectNormalizedName": "Foo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "project/create",
|
|
||||||
"id": 2,
|
|
||||||
"params": {
|
|
||||||
"name": "Foo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client.expectJson(json"""
|
|
||||||
{
|
|
||||||
"jsonrpc":"2.0",
|
|
||||||
"id":2,
|
|
||||||
"error":{
|
|
||||||
"code":4003,
|
|
||||||
"message":"Project with the provided name exists"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
//teardown
|
|
||||||
deleteProject(projectId)
|
|
||||||
}
|
|
||||||
|
|
||||||
"create project structure" in {
|
"create project structure" in {
|
||||||
val projectName = "Foo"
|
val projectName = "Foo"
|
||||||
|
|
||||||
@ -276,6 +229,27 @@ class ProjectManagementApiSpec
|
|||||||
deleteProject(projectId)
|
deleteProject(projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"find a name when project with the same name exists" in {
|
||||||
|
val projectName = "Foo"
|
||||||
|
|
||||||
|
implicit val client: WsTestClient = new WsTestClient(address)
|
||||||
|
|
||||||
|
val projectId1 = createProject(projectName)
|
||||||
|
val projectId2 = createProject(
|
||||||
|
projectName,
|
||||||
|
nameSuffix = Some(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
val projectDir = new File(userProjectDir, "Foo_1")
|
||||||
|
val packageFile = new File(projectDir, "package.yaml")
|
||||||
|
|
||||||
|
Files.readAllLines(packageFile.toPath) contains "name: Foo_1"
|
||||||
|
|
||||||
|
//teardown
|
||||||
|
deleteProject(projectId1)
|
||||||
|
deleteProject(projectId2)
|
||||||
|
}
|
||||||
|
|
||||||
"find a name when project is created from template" in {
|
"find a name when project is created from template" in {
|
||||||
val projectName = "Foo"
|
val projectName = "Foo"
|
||||||
|
|
||||||
@ -307,7 +281,7 @@ class ProjectManagementApiSpec
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"params": {
|
"params": {
|
||||||
"name": "Foo",
|
"name": "Foo",
|
||||||
"version": ${CurrentVersion.version.toString()}
|
"version": ${CurrentVersion.version.toString}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
@ -582,13 +556,14 @@ class ProjectManagementApiSpec
|
|||||||
|
|
||||||
"fail when project's edition could not be resolved" in {
|
"fail when project's edition could not be resolved" in {
|
||||||
pending
|
pending
|
||||||
implicit val client = new WsTestClient(address)
|
implicit val client: WsTestClient = new WsTestClient(address)
|
||||||
|
//given
|
||||||
val projectId = createProject("Foo")
|
val projectId = createProject("Foo")
|
||||||
setProjectParentEdition(
|
setProjectParentEdition(
|
||||||
"Foo",
|
"Foo",
|
||||||
"some_weird_edition_name_that-surely-does-not-exist"
|
"some_weird_edition_name_that-surely-does-not-exist"
|
||||||
)
|
)
|
||||||
|
//when
|
||||||
client.send(json"""
|
client.send(json"""
|
||||||
{ "jsonrpc": "2.0",
|
{ "jsonrpc": "2.0",
|
||||||
"method": "project/open",
|
"method": "project/open",
|
||||||
@ -598,6 +573,7 @@ class ProjectManagementApiSpec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
//then
|
||||||
client.expectJson(json"""
|
client.expectJson(json"""
|
||||||
{
|
{
|
||||||
"jsonrpc":"2.0",
|
"jsonrpc":"2.0",
|
||||||
@ -608,7 +584,7 @@ class ProjectManagementApiSpec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
//teardown
|
||||||
deleteProject(projectId)
|
deleteProject(projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1347,4 +1323,130 @@ class ProjectManagementApiSpec
|
|||||||
deleteProject(projectId)
|
deleteProject(projectId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"project/duplicate" must {
|
||||||
|
|
||||||
|
"duplicate a project" in {
|
||||||
|
implicit val client: WsTestClient = new WsTestClient(address)
|
||||||
|
//given
|
||||||
|
val projectName = "Project To Copy"
|
||||||
|
val projectId = createProject(projectName)
|
||||||
|
//when
|
||||||
|
client.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "project/duplicate",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"projectId": $projectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
//then
|
||||||
|
val newProjectName = "Project To Copy (copy)"
|
||||||
|
val duplicateReply = client.fuzzyExpectJson(json"""
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 0,
|
||||||
|
"result": {
|
||||||
|
"projectId": "*",
|
||||||
|
"projectName": $newProjectName,
|
||||||
|
"projectNormalizedName": "ProjectToCopycopy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
val Some(duplicatedProjectId) = for {
|
||||||
|
reply <- duplicateReply.asObject
|
||||||
|
resultJson <- reply("result")
|
||||||
|
result <- resultJson.asObject
|
||||||
|
projectIdJson <- result("projectId")
|
||||||
|
projectId <- projectIdJson.asString
|
||||||
|
} yield UUID.fromString(projectId)
|
||||||
|
|
||||||
|
{
|
||||||
|
val projectDir = new File(userProjectDir, "ProjectToCopycopy")
|
||||||
|
val packageFile = new File(projectDir, "package.yaml")
|
||||||
|
val buffer = Source.fromFile(packageFile)
|
||||||
|
try {
|
||||||
|
val lines = buffer.getLines()
|
||||||
|
lines.contains(s"name: $newProjectName") shouldBe true
|
||||||
|
} finally {
|
||||||
|
buffer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//when
|
||||||
|
client.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "project/duplicate",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"projectId": $projectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
//then
|
||||||
|
val newProjectName1 = "Project To Copy (copy)_1"
|
||||||
|
val duplicateReply1 = client.fuzzyExpectJson(json"""
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 0,
|
||||||
|
"result": {
|
||||||
|
"projectId": "*",
|
||||||
|
"projectName": $newProjectName1,
|
||||||
|
"projectNormalizedName": "ProjectToCopycopy_1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
val Some(duplicatedProjectId1) = for {
|
||||||
|
reply <- duplicateReply1.asObject
|
||||||
|
resultJson <- reply("result")
|
||||||
|
result <- resultJson.asObject
|
||||||
|
projectIdJson <- result("projectId")
|
||||||
|
projectId <- projectIdJson.asString
|
||||||
|
} yield UUID.fromString(projectId)
|
||||||
|
|
||||||
|
{
|
||||||
|
val projectDir = new File(userProjectDir, "ProjectToCopycopy_1")
|
||||||
|
val packageFile = new File(projectDir, "package.yaml")
|
||||||
|
val buffer = Source.fromFile(packageFile)
|
||||||
|
try {
|
||||||
|
val lines = buffer.getLines()
|
||||||
|
lines.contains(s"name: $newProjectName1") shouldBe true
|
||||||
|
} finally {
|
||||||
|
buffer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//teardown
|
||||||
|
deleteProject(duplicatedProjectId)
|
||||||
|
deleteProject(duplicatedProjectId1)
|
||||||
|
deleteProject(projectId)
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail when project doesn't exist" in {
|
||||||
|
val client = new WsTestClient(address)
|
||||||
|
client.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "project/duplicate",
|
||||||
|
"id": 1,
|
||||||
|
"params": {
|
||||||
|
"projectId": ${UUID.randomUUID()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
client.expectJson(json"""
|
||||||
|
{
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"id":1,
|
||||||
|
"error":{
|
||||||
|
"code":4004,
|
||||||
|
"message":"Project with the provided id does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,52 @@ class FileSystemServiceSpec
|
|||||||
FileUtils.deleteQuietly(targetPath)
|
FileUtils.deleteQuietly(targetPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"copy file" in {
|
||||||
|
val testDir = testStorageConfig.userProjectsPath
|
||||||
|
|
||||||
|
val targetFileName = "target_copy_file.txt"
|
||||||
|
val destinationFileName = "destination_copy_file.txt"
|
||||||
|
val targetFilePath = new File(testDir, targetFileName)
|
||||||
|
val destinationFilePath = new File(testDir, destinationFileName)
|
||||||
|
|
||||||
|
FileUtils.forceMkdirParent(targetFilePath)
|
||||||
|
FileUtils.touch(targetFilePath)
|
||||||
|
|
||||||
|
fileSystemService
|
||||||
|
.copy(targetFilePath, destinationFilePath)
|
||||||
|
.unsafeRunSync()
|
||||||
|
|
||||||
|
Files.exists(targetFilePath.toPath) shouldEqual true
|
||||||
|
Files.exists(destinationFilePath.toPath) shouldEqual true
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
FileUtils.deleteQuietly(targetFilePath)
|
||||||
|
FileUtils.deleteQuietly(destinationFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
"copy directory" in {
|
||||||
|
implicit val client: WsTestClient = new WsTestClient(address)
|
||||||
|
|
||||||
|
val testDir = testStorageConfig.userProjectsPath
|
||||||
|
|
||||||
|
val projectName = "New_Project_To_Copy"
|
||||||
|
createProject(projectName)
|
||||||
|
|
||||||
|
val directoryPath = new File(testDir, projectName)
|
||||||
|
val targetPath = new File(testDir, "Target_Copy_Directory")
|
||||||
|
|
||||||
|
fileSystemService
|
||||||
|
.copy(directoryPath, targetPath)
|
||||||
|
.unsafeRunSync()
|
||||||
|
|
||||||
|
Files.exists(directoryPath.toPath) shouldEqual true
|
||||||
|
Files.isDirectory(targetPath.toPath) shouldEqual true
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
FileUtils.deleteQuietly(directoryPath)
|
||||||
|
FileUtils.deleteQuietly(targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
"write path" in {
|
"write path" in {
|
||||||
val testDir = testStorageConfig.userProjectsPath
|
val testDir = testStorageConfig.userProjectsPath
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user