mirror of
https://github.com/enso-org/enso.git
synced 2024-10-05 17:17:50 +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/listSample`](#projectlistsample)
|
||||
- [`project/status`](#projectstatus)
|
||||
- [`project/duplicate`](#projectduplicate)
|
||||
- [Action Progress Reporting](#action-progress-reporting)
|
||||
- [`task/started`](#taskstarted)
|
||||
- [`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
|
||||
|
||||
Some actions, especially those related to installation of new components may
|
||||
|
@ -59,7 +59,7 @@ abstract class JsonRpcServerTestKit
|
||||
|
||||
def clientControllerFactory(): ClientControllerFactory
|
||||
|
||||
var _clientControllerFactory: ClientControllerFactory = _
|
||||
private var _clientControllerFactory: ClientControllerFactory = _
|
||||
|
||||
override def beforeEach(): Unit = {
|
||||
super.beforeEach()
|
||||
@ -67,7 +67,7 @@ abstract class JsonRpcServerTestKit
|
||||
factory.init()
|
||||
_clientControllerFactory = 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}"
|
||||
}
|
||||
|
||||
@ -174,10 +174,12 @@ abstract class JsonRpcServerTestKit
|
||||
def fuzzyExpectJson(
|
||||
json: Json,
|
||||
timeout: FiniteDuration = 5.seconds.dilated
|
||||
)(implicit pos: Position): Assertion = {
|
||||
)(implicit pos: Position): Json = {
|
||||
val parsed = parse(expectMessage(timeout))
|
||||
|
||||
parsed should fuzzyMatchJson(json)
|
||||
|
||||
inside(parsed) { case Right(json) => json }
|
||||
}
|
||||
|
||||
def expectNoMessage(): Unit = outActor.expectNoMessage()
|
||||
@ -191,9 +193,10 @@ abstract class JsonRpcServerTestKit
|
||||
trait FuzzyJsonMatchers { self: Matchers =>
|
||||
class JsonEquals(expected: 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 expectedFormatted = patch[scala.util.Try](expected)
|
||||
MatchResult(
|
||||
|
@ -89,6 +89,20 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
|
||||
}
|
||||
.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 */
|
||||
override def exists(file: File): F[FileSystemFailure, Boolean] =
|
||||
Sync[F]
|
||||
|
@ -48,7 +48,7 @@ trait FileSystem[F[+_, +_]] {
|
||||
*/
|
||||
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 to a path to the destination
|
||||
@ -56,6 +56,14 @@ trait FileSystem[F[+_, +_]] {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param file the file to check
|
||||
|
@ -5,6 +5,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.util.UUID
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
import org.enso.pkg.validation.NameValidation
|
||||
import org.enso.projectmanager.boot.configuration.MetadataStorageConfig
|
||||
import org.enso.projectmanager.control.core.{
|
||||
Applicative,
|
||||
@ -207,14 +208,7 @@ class ProjectFileRepository[
|
||||
/** @inheritdoc */
|
||||
def update(project: Project): F[ProjectRepositoryFailure, Unit] =
|
||||
metadataStorage(project.path)
|
||||
.persist(
|
||||
ProjectMetadata(
|
||||
id = project.id,
|
||||
kind = project.kind,
|
||||
created = project.created,
|
||||
lastOpened = project.lastOpened
|
||||
)
|
||||
)
|
||||
.persist(ProjectMetadata(project))
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -231,46 +225,55 @@ class ProjectFileRepository[
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def moveProjectToTargetDir(
|
||||
override def moveProject(
|
||||
projectId: UUID,
|
||||
newName: String
|
||||
): F[ProjectRepositoryFailure, File] = {
|
||||
def move(project: Project) =
|
||||
for {
|
||||
targetPath <- findTargetPath(newName)
|
||||
_ <- moveProjectDir(project, targetPath)
|
||||
targetPath <- findTargetPath(NameValidation.normalizeName(newName))
|
||||
_ <- moveProjectDir(project.path, targetPath)
|
||||
} yield targetPath
|
||||
|
||||
for {
|
||||
project <- getProject(projectId)
|
||||
primaryPath = new File(projectsPath, newName)
|
||||
finalPath <-
|
||||
if (isLocationOk(project.path, primaryPath)) {
|
||||
CovariantFlatMap[F].pure(primaryPath)
|
||||
} else {
|
||||
move(project)
|
||||
}
|
||||
} yield finalPath
|
||||
project <- getProject(projectId)
|
||||
projectPath <- move(project)
|
||||
} yield projectPath
|
||||
}
|
||||
|
||||
private def isLocationOk(
|
||||
currentFile: File,
|
||||
primaryFile: File
|
||||
): Boolean = {
|
||||
val currentPath = currentFile.toString
|
||||
val primaryPath = primaryFile.toString
|
||||
if (currentPath.startsWith(primaryPath)) {
|
||||
val suffixPattern = "_\\d+"
|
||||
val suffix = currentPath.substring(primaryPath.length, currentPath.length)
|
||||
suffix.matches(suffixPattern)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
/** @inheritdoc */
|
||||
override def copyProject(
|
||||
project: Project,
|
||||
newName: String,
|
||||
newMetadata: ProjectMetadata
|
||||
): F[ProjectRepositoryFailure, Project] = {
|
||||
def copy(project: Project) =
|
||||
for {
|
||||
targetPath <- findTargetPath(NameValidation.normalizeName(newName))
|
||||
_ <- copyProjectDir(project.path, targetPath)
|
||||
} yield targetPath
|
||||
|
||||
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
|
||||
.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 =>
|
||||
StorageFailure(failure.toString)
|
||||
)
|
||||
|
@ -3,8 +3,7 @@ package org.enso.projectmanager.infrastructure.repository
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.projectmanager.model.Project
|
||||
import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
||||
|
||||
/** An abstraction for accessing project domain objects from durable storage.
|
||||
*
|
||||
@ -82,11 +81,23 @@ trait ProjectRepository[F[+_, +_]] {
|
||||
* @param projectId the project id
|
||||
* @param newName the new project name
|
||||
*/
|
||||
def moveProjectToTargetDir(
|
||||
def moveProject(
|
||||
projectId: UUID,
|
||||
newName: String
|
||||
): 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.
|
||||
*
|
||||
* @param projectId the project id
|
||||
|
@ -86,6 +86,11 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel: Sync](
|
||||
),
|
||||
ProjectRename -> ProjectRenameHandler
|
||||
.props[F](projectService, timeoutConfig.requestTimeout),
|
||||
ProjectDuplicate -> ProjectDuplicateHandler.props[F](
|
||||
projectService,
|
||||
timeoutConfig.requestTimeout,
|
||||
timeoutConfig.retries
|
||||
),
|
||||
EngineListInstalled -> EngineListInstalledHandler.props(
|
||||
runtimeVersionManagementService,
|
||||
timeoutConfig.requestTimeout
|
||||
|
@ -26,6 +26,7 @@ object JsonRpc {
|
||||
.registerRequest(ProjectClose)
|
||||
.registerRequest(ProjectRename)
|
||||
.registerRequest(ProjectList)
|
||||
.registerRequest(ProjectDuplicate)
|
||||
.registerNotification(TaskStarted)
|
||||
.registerNotification(TaskProgressUpdate)
|
||||
.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 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() =
|
||||
for {
|
||||
_ <- log.debug("Moving project [{}] to [{}].", projectId, newName)
|
||||
dir <- repo.moveProjectToTargetDir(projectId, newName)
|
||||
dir <- repo.moveProject(projectId, newName)
|
||||
_ <- log.info("Project [{}] moved to [{}].", projectId, dir)
|
||||
} yield ()
|
||||
|
||||
|
@ -33,6 +33,7 @@ import org.enso.projectmanager.infrastructure.repository.{
|
||||
ProjectRepositoryFailure
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.time.Clock
|
||||
import org.enso.projectmanager.model
|
||||
import org.enso.projectmanager.model.Project
|
||||
import org.enso.projectmanager.model.ProjectKinds.UserProject
|
||||
import org.enso.projectmanager.service.ProjectServiceFailure._
|
||||
@ -96,7 +97,7 @@ class ProjectService[
|
||||
projectsDirectory
|
||||
)
|
||||
repo = projectRepositoryFactory.getProjectRepository(projectsDirectory)
|
||||
name <- getNameForNewProject(projectName, projectTemplate, repo)
|
||||
name <- getNameForNewProject(projectName, repo)
|
||||
_ <- log.info("Created project with actual name [{}].", name)
|
||||
_ <- validateProjectName(name)
|
||||
_ <- 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 */
|
||||
override def listProjects(
|
||||
maybeSize: Option[Int]
|
||||
@ -474,7 +502,6 @@ class ProjectService[
|
||||
|
||||
private def getNameForNewProject(
|
||||
projectName: String,
|
||||
projectTemplate: Option[String],
|
||||
projectRepository: ProjectRepository[F]
|
||||
): F[ProjectServiceFailure, String] = {
|
||||
def mkName(name: String, suffix: Int): String =
|
||||
@ -490,20 +517,17 @@ class ProjectService[
|
||||
)
|
||||
}
|
||||
|
||||
projectTemplate match {
|
||||
case Some(_) =>
|
||||
CovariantFlatMap[F]
|
||||
.ifM(projectRepository.exists(projectName))(
|
||||
ifTrue = findAvailableName(projectName, 1),
|
||||
ifFalse = CovariantFlatMap[F].pure(projectName)
|
||||
)
|
||||
.mapError(toServiceFailure)
|
||||
case None =>
|
||||
CovariantFlatMap[F].pure(projectName)
|
||||
}
|
||||
|
||||
CovariantFlatMap[F]
|
||||
.ifM(projectRepository.exists(projectName))(
|
||||
ifTrue = findAvailableName(projectName, 1),
|
||||
ifFalse = CovariantFlatMap[F].pure(projectName)
|
||||
)
|
||||
.mapError(toServiceFailure)
|
||||
}
|
||||
|
||||
private def getNameForDuplicatedProject(projectName: String): String =
|
||||
s"$projectName (copy)"
|
||||
|
||||
/** Retrieve project info.
|
||||
*
|
||||
* @param clientId the requester id
|
||||
|
@ -97,6 +97,17 @@ trait ProjectServiceApi[F[+_, +_]] {
|
||||
projectId: UUID
|
||||
): 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..
|
||||
*
|
||||
* @param maybeSize the size of result set
|
||||
|
@ -56,6 +56,12 @@ class FileSystemService[F[+_, +_]: Applicative: CovariantFlatMap: ErrorChannel](
|
||||
.move(from, to)
|
||||
.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 */
|
||||
override def write(
|
||||
path: File,
|
||||
|
@ -30,6 +30,13 @@ trait FileSystemServiceApi[F[+_, +_]] {
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @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 {
|
||||
val projectName = "Foo"
|
||||
|
||||
@ -276,6 +229,27 @@ class ProjectManagementApiSpec
|
||||
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 {
|
||||
val projectName = "Foo"
|
||||
|
||||
@ -307,7 +281,7 @@ class ProjectManagementApiSpec
|
||||
"id": 1,
|
||||
"params": {
|
||||
"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 {
|
||||
pending
|
||||
implicit val client = new WsTestClient(address)
|
||||
val projectId = createProject("Foo")
|
||||
implicit val client: WsTestClient = new WsTestClient(address)
|
||||
//given
|
||||
val projectId = createProject("Foo")
|
||||
setProjectParentEdition(
|
||||
"Foo",
|
||||
"some_weird_edition_name_that-surely-does-not-exist"
|
||||
)
|
||||
|
||||
//when
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "project/open",
|
||||
@ -598,6 +573,7 @@ class ProjectManagementApiSpec
|
||||
}
|
||||
}
|
||||
""")
|
||||
//then
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
@ -608,7 +584,7 @@ class ProjectManagementApiSpec
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
//teardown
|
||||
deleteProject(projectId)
|
||||
}
|
||||
|
||||
@ -1347,4 +1323,130 @@ class ProjectManagementApiSpec
|
||||
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)
|
||||
}
|
||||
|
||||
"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 {
|
||||
val testDir = testStorageConfig.userProjectsPath
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user