Skip to content

Commit

Permalink
Add project/duplicate endpoint (#10407)
Browse files Browse the repository at this point in the history
close #10367

Changelog:
- add: `project/duplicate` project manager command to duplicate existing projects
  • Loading branch information
4e6 committed Jul 2, 2024
1 parent 69b5f71 commit 764259f
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 111 deletions.
45 changes: 45 additions & 0 deletions docs/language-server/protocol-project-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ abstract class JsonRpcServerTestKit

def clientControllerFactory(): ClientControllerFactory

var _clientControllerFactory: ClientControllerFactory = _
private var _clientControllerFactory: ClientControllerFactory = _

override def beforeEach(): Unit = {
super.beforeEach()
val factory = protocolFactory
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}"
}

Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,22 @@ 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
* @return either [[FileSystemFailure]] or 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.
*
* @param file the file to check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 */
Expand All @@ -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(projectPath: File, targetPath: File) = {
fileSystem
.move(projectPath, targetPath)
.mapError[ProjectRepositoryFailure](failure =>
StorageFailure(failure.toString)
)
}

private def moveProjectDir(project: Project, targetPath: File) = {
private def copyProjectDir(projectPath: File, targetPath: File) = {
fileSystem
.move(project.path, targetPath)
.copy(projectPath, targetPath)
.mapError[ProjectRepositoryFailure](failure =>
StorageFailure(failure.toString)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ object JsonRpc {
.registerRequest(ProjectClose)
.registerRequest(ProjectRename)
.registerRequest(ProjectList)
.registerRequest(ProjectDuplicate)
.registerNotification(TaskStarted)
.registerNotification(TaskProgressUpdate)
.registerNotification(TaskFinished)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 764259f

Please sign in to comment.