backend/app/controllers/api/Workspaces.scala (273 lines of code) (raw):
package controllers.api
import commands.DeleteResource
import model.Uri
import model.annotations.{Workspace, WorkspaceEntry, WorkspaceLeaf}
import model.frontend.{TreeEntry, TreeLeaf, TreeNode}
import net.logstash.logback.marker.LogstashMarker
import net.logstash.logback.marker.Markers.append
import play.api.libs.json._
import services.ObjectStorage
import services.manifest.Manifest
import services.observability.PostgresClient
import services.annotations.Annotations
import services.annotations.Annotations.AffectedResource
import services.index.Index
import services.users.UserManagement
import utils.Logging
import utils.attempt._
import utils.auth.{User, UserIdentityRequest}
import utils.controller.{AuthApiController, AuthControllerComponents}
import java.util.UUID
case class CreateWorkspaceData(name: String, isPublic: Boolean, tagColor: String)
object CreateWorkspaceData {
implicit val format = Json.format[CreateWorkspaceData]
}
case class UpdateWorkspaceFollowers(followers: List[String])
object UpdateWorkspaceFollowers {
implicit val format = Json.format[UpdateWorkspaceFollowers]
}
case class UpdateWorkspaceIsPublic(isPublic: Boolean)
object UpdateWorkspaceIsPublic {
implicit val format = Json.format[UpdateWorkspaceIsPublic]
}
case class UpdateWorkspaceName(name: String)
object UpdateWorkspaceName {
implicit val format = Json.format[UpdateWorkspaceName]
}
case class AddItemParameters(uri: Option[String], size: Option[Long], mimeType: Option[String])
object AddItemParameters {
implicit val format = Json.format[AddItemParameters]
}
case class AddItemData(name: String, parentId: String, `type`: String, icon: Option[String], parameters: AddItemParameters)
object AddItemData {
implicit val format = Json.format[AddItemData]
}
case class RenameItemData(name: String)
object RenameItemData {
implicit val format = Json.format[RenameItemData]
}
case class MoveCopyDestination(newParentId: Option[String], newWorkspaceId: Option[String])
object MoveCopyDestination {
implicit val format = Json.format[MoveCopyDestination]
}
class Workspaces(override val controllerComponents: AuthControllerComponents, annotation: Annotations, index: Index, manifest: Manifest,
users: UserManagement, objectStorage: ObjectStorage, previewStorage: ObjectStorage, postgresClient: PostgresClient) extends AuthApiController with Logging {
def create = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[CreateWorkspaceData].toAttempt
id = UUID.randomUUID().toString
_ = logAction(req.user, id, s"Creating new workspace. ID: $id. Name: ${data.name}. IsPublic: ${data.isPublic}")
_ <- annotation.insertWorkspace(req.user.username, id, data.name, data.isPublic, data.tagColor)
} yield {
Created(id)
}
}
def getAll = ApiAction.attempt { req: UserIdentityRequest[_] =>
annotation.getAllWorkspacesMetadata(req.user.username)
.map(workspaces => Ok(Json.toJson(workspaces)))
}
private def reprocessBlob(uri: Uri, rerunSuccessful: Boolean, rerunFailed: Boolean): Attempt[Unit] = for {
_ <- if(rerunFailed) { manifest.rerunFailedExtractorsForBlob(uri) } else { Attempt.Right(()) }
_ <- if(rerunSuccessful) { manifest.rerunSuccessfulExtractorsForBlob(uri) } else { Attempt.Right(()) }
} yield {
()
}
// Execute in series rather than in parallel,
// to avoid locking issues between successive blobs
private def reprocessBlobs(blobIds: List[Uri], rerunSuccesful: Boolean, rerunFailed: Boolean): Attempt[Unit] = {
blobIds match {
case Nil => Attempt.Right(())
case blobId :: Nil => reprocessBlob(blobId, rerunSuccesful, rerunFailed)
case blobId :: tail => reprocessBlob(blobId, rerunSuccesful, rerunFailed).flatMap(_ =>
reprocessBlobs(tail, rerunSuccesful, rerunFailed)
)
}
}
// For all blobs that are referenced by nodes in this workspace,
// re-process those blobs by putting neo4j in a state where
// first all previously failed extractors will re-run,
// and then all previously successful extractors will re-run
def reprocess(workspaceId: String, rerunSuccessfulParam: Option[Boolean], rerunFailedParam: Option[Boolean]) = ApiAction.attempt { req: UserIdentityRequest[_] =>
val rerunSuccessful = rerunSuccessfulParam.getOrElse(true)
val rerunFailed = rerunFailedParam.getOrElse(true)
for {
contents <- annotation.getWorkspaceContents(req.user.username, workspaceId)
blobIds = TreeEntry.workspaceTreeToBlobIds(contents)
_ <- reprocessBlobs(blobIds, rerunSuccessful, rerunFailed)
} yield {
Ok(Json.toJson(blobIds))
}
}
def get(workspaceId: String) = ApiAction.attempt { req: UserIdentityRequest[_] =>
for {
metadata <- annotation.getWorkspaceMetadata(req.user.username, workspaceId)
contents <- annotation.getWorkspaceContents(req.user.username, workspaceId)
} yield {
Ok(Json.toJson(Workspace.fromMetadataAndRootNode(metadata, contents)))
}
}
def getContents(workspaceId: String) = ApiAction.attempt { req =>
annotation.getWorkspaceContents(req.user.username, workspaceId)
.map(workspace => Ok(Json.toJson(workspace)))
}
def updateWorkspaceFollowers(workspaceId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[UpdateWorkspaceFollowers].toAttempt
_ = logAction(req.user, workspaceId, s"Set workspace followers. Data: $data")
_ <- annotation.updateWorkspaceFollowers(
req.user.username,
workspaceId,
data.followers
)
} yield {
NoContent
}
}
def updateWorkspaceIsPublic(workspaceId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[UpdateWorkspaceIsPublic].toAttempt
_ = logAction(req.user, workspaceId, s"Set workspace isPublic. Data: $data")
_ <- annotation.updateWorkspaceIsPublic(
req.user.username,
workspaceId,
data.isPublic
)
} yield {
NoContent
}
}
def updateWorkspaceName(workspaceId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[UpdateWorkspaceName].toAttempt
_ = logAction(req.user, workspaceId, s"Set workspace name. Data: $data")
_ <- annotation.updateWorkspaceName(
req.user.username,
workspaceId,
data.name
)
} yield {
NoContent
}
}
def deleteWorkspace(workspaceId: String) = ApiAction.attempt { req: UserIdentityRequest[_] =>
logAction(req.user, workspaceId, s"Delete workspace. ID: $workspaceId")
for {
_ <- annotation.deleteWorkspace(req.user.username, workspaceId)
_ <- index.deleteWorkspace(workspaceId)
} yield {
NoContent
}
}
private def insertItem(username: String, workspaceId: String, workspaceNodeId: String, data: AddItemData): Attempt[String] = {
if (data.`type` == "folder") {
annotation.addFolder(username, workspaceId, data.parentId, data.name)
} else {
val blobUri = data.parameters.uri.map(Uri(_)).get
for {
_ <- annotation.addResourceToWorkspaceFolder(username, data.name, blobUri, data.parameters.size, data.parameters.mimeType, data.icon.get, workspaceId, data.parentId, workspaceNodeId)
_ <- index.addResourceToWorkspace(blobUri, workspaceId, workspaceNodeId)
} yield {
workspaceNodeId
}
}
}
private def copyTree(workspaceId: String, destinationParentId: String, tree: TreeEntry[WorkspaceEntry], user: String): Attempt[List[String]] = {
val newId = UUID.randomUUID().toString
tree match {
case TreeLeaf(_, name, data, _) =>
// a TreeLeaf won't have any children, so just insert the item at the destination location, and return it's new ID
data match {
case WorkspaceLeaf(_, _, _, _, uri, mimeType, size) =>
val addItemData = AddItemData(name, destinationParentId, "file", Some("document"), AddItemParameters(Some(uri), size, Some(mimeType)))
insertItem(user, workspaceId, newId, addItemData).map(i => List(i))
case _ => Attempt.Left(WorkspaceCopyFailure("Unexpected data type of TreeLeaf"))
}
case TreeNode(_, name, _, children) =>
// TreeNodes are folders. We need to create the folder in the new destination, and then recurse on every child item
// create the folder in the destination location
val addItemData = AddItemData(name, destinationParentId, "folder", None, AddItemParameters(None, None, None))
val newFolderIdAttempt = insertItem(user, workspaceId, newId, addItemData)
// for every child, recurse, setting the newly created folder as the destination
val newChildIds = newFolderIdAttempt.flatMap{ newFolderId =>
Attempt.traverse(children)(child => copyTree(workspaceId, newFolderId, child, user))
}
// return ids of all child nodes combined with the id of the new folder
newChildIds.map(ids => newId +: ids.flatten )
}
}
def addItemToWorkspace(workspaceId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[AddItemData].toAttempt
itemId = UUID.randomUUID().toString
_ = logAction(req.user, workspaceId, s"Add item to workspace. Node ID: $itemId. Data: $data")
id <- insertItem(req.user.username, workspaceId, itemId, data)
} yield {
Created(Json.obj("id" -> id))
}
}
def renameItem(workspaceId: String, itemId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[RenameItemData].toAttempt
_ = logAction(req.user, workspaceId, s"Rename workspace item. Node ID: $itemId. Data: $data")
_ <- annotation.renameWorkspaceItem(req.user.username, workspaceId, itemId, data.name)
} yield {
NoContent
}
}
private def updateIndex(resourceMoved: AffectedResource, oldWorkspaceId: String, newWorkspaceId: Option[String]): Attempt[Unit] = {
for {
_ <- index.removeResourceFromWorkspace(
resourceMoved.uri,
oldWorkspaceId,
resourceMoved.workspaceNodeId
)
_ <- index.addResourceToWorkspace(
resourceMoved.uri,
newWorkspaceId.getOrElse(oldWorkspaceId),
resourceMoved.workspaceNodeId
)
} yield {
()
}
}
def moveItem(workspaceId: String, itemId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[MoveCopyDestination].toAttempt
_ = logAction(req.user, workspaceId, s"Move workspace item. Node ID: $itemId. Data: $data")
_ <- if (data.newParentId.contains(itemId)) Attempt.Left(ClientFailure("Cannot move a workspace item to be under itself")) else Attempt.Right(())
result <- annotation.moveWorkspaceItem(req.user.username, workspaceId, itemId, data.newWorkspaceId, data.newParentId)
_ <- Attempt.traverse(result.resourcesMoved) { resourceMoved =>
updateIndex(resourceMoved, workspaceId, data.newWorkspaceId)
}
} yield {
NoContent
}
}
def copyItem(workspaceId: String, itemId: String) = ApiAction.attempt(parse.json) { req =>
for {
data <- req.body.validate[MoveCopyDestination].toAttempt
_ = logAction(req.user, workspaceId, s"Copy workspace item. Node ID: $itemId. Data: $data")
_ <- if (data.newParentId.contains(itemId)) Attempt.Left(ClientFailure("Cannot copy a workspace item to the same location")) else Attempt.Right(())
copyDestination <-annotation.getCopyDestination(req.user.username, workspaceId, data.newWorkspaceId, data.newParentId)
workspaceContents <- annotation.getWorkspaceContents(req.user.username, workspaceId)
_ <- TreeEntry.findNodeById(workspaceContents, itemId)
.map(entry => copyTree(copyDestination.workspaceId, copyDestination.parentId, entry, req.user.username)).getOrElse(Attempt.Left(ClientFailure("Must supply at least one of newWorkspaceId or newParentId")))
} yield {
NoContent
}
}
def removeItem(workspaceId: String, itemId: String) = ApiAction.attempt { req =>
logAction(req.user, workspaceId, s"Rename workspace item. Node ID: $itemId")
for {
result <- annotation.deleteWorkspaceItem(req.user.username, workspaceId, itemId)
_ <- Attempt.sequence(result.resourcesRemoved.map(r => index.removeResourceFromWorkspace(r.uri, workspaceId, r.workspaceNodeId)))
} yield {
NoContent
}
}
def deleteBlob(workspaceId: String, blobUri: String) = ApiAction.attempt { req =>
annotation.getBlobOwners(blobUri).flatMap { owners =>
if (owners.size == 1 && owners.head == req.user.username) {
logAction(req.user, workspaceId, s"Deleting resource from Giant if no children. Resource uri: $blobUri")
val deleteResource = new DeleteResource(manifest, index, previewStorage, objectStorage, postgresClient)
deleteResource.deleteBlobCheckChildren(blobUri)
} else {
logAction(req.user, workspaceId, s"Can't delete resource due to file ownership conflict. Resource uri: $blobUri")
Attempt.Left[Unit](DeleteNotAllowed("Failed to delete resource"))
}
} map (_ => NoContent)
}
private def logAction(user: User, workspaceId: String, message: String) = {
val markers: LogstashMarker = user.asLogMarker.and(append("workspaceId", workspaceId))
logger.info(markers, message)
}
}