backend/app/commands/GetResource.scala (128 lines of code) (raw):
package commands
import com.google.common.net.PercentEscaper
import model.Uri
import model.frontend.{BasicResource, DocumentResource, EmailResource, Resource}
import services.annotations.Annotations
import services.index.Index
import services.manifest.Manifest
import services.users.UserManagement
import utils.attempt.{Attempt, ContentTooLongFailure, NotFoundFailure}
import scala.concurrent.ExecutionContext
sealed trait ResourceFetchMode
object ResourceFetchMode {
case object Basic extends ResourceFetchMode
case class WithData(query: Option[String]) extends ResourceFetchMode
}
case class GetResource(uri: Uri, mode: ResourceFetchMode, username: String, manifest: Manifest, index: Index, annotations: Annotations, users: UserManagement)(implicit ec: ExecutionContext) extends AttemptCommand[Resource] {
override def process(): Attempt[Resource] = for {
visibleCollections <- users.getVisibleCollectionUrisForUser(username)
basicResource <- manifest.getResource(uri).toAttempt.flatMap(validatePermissions(_, visibleCollections))
fullResource <- mergeResourcesIfRequired(basicResource)
topLevelParent <- fetchTopLevelParent(fullResource)
} yield {
GetResource.formatResourceForClient(fullResource, topLevelParent)
}
private def validatePermissions(resource: BasicResource, visibleCollections: Set[String]): Attempt[BasicResource] = {
val urisOfResourceAndParents = (resource.uri :: resource.parents.map(_.uri)).toSet
val rootUrisOfResourceAndParents = urisOfResourceAndParents.map(Uri(_).root)
if (visibleCollections.intersect(rootUrisOfResourceAndParents).nonEmpty) {
Attempt.Right(resource)
} else {
annotations.getAllWorkspacesMetadata(username).flatMap { visibleWorkspaces =>
index.anyWorkspaceOrCollectionContainsAnyResource(
collectionUris = visibleCollections,
workspaceIds = visibleWorkspaces.map(_.id).toSet,
resourceUris = rootUrisOfResourceAndParents
) flatMap {
case true => Attempt.Right(resource)
case false => Attempt.Left(NotFoundFailure(s"${resource.uri} does not exist"))
}
}
}
}
private def mergeResourcesIfRequired(resource: BasicResource): Attempt[Resource] = mode match {
case ResourceFetchMode.Basic =>
Attempt.Right(resource)
case ResourceFetchMode.WithData(query) =>
index.getPageCount(uri).flatMap {
// From testing we know that page counts over 500 start to run into
// rendering difficulties in the browser.
case Some(pageCount) if pageCount > 500 => Attempt.Right(resource)
case _ => (for {
indexed <- index.getResource(uri, query)
comments <- annotations.getComments(uri)
} yield {
Resource.mergeResources(indexed, resource, comments)
}).recoverWith {
case _: ContentTooLongFailure =>
// fall back to a basic response
Attempt.Right(resource)
}
}
}
// This is to show the name of a blob at the top of the breadcrumb trail when browsing within a zip file.
// The user may be able to see multiple top-level parents (for example the same ZIP file in different places in
// the same ingestion) in which case we pick the first one they can see as a compromise.
private def fetchTopLevelParent(resource: Resource): Attempt[Option[String]] = {
resource.parents.headOption.flatMap(_.uri.split('/').headOption) match {
case Some(parent) =>
Attempt.fromEither(manifest.getResource(Uri(parent))).map { topLevelParent =>
if(topLevelParent.`type` == "blob" && topLevelParent.parents.nonEmpty) {
Some(topLevelParent.parents.head.uri.split('/').last)
} else {
Some(parent)
}
}
case None =>
Attempt.Right(None)
}
}
}
object GetResource {
// Java URLEncoder encodes spaces as + and not %20 which is what we need on the client side so we use the
// Guava urlFragmentEscaper instead. We customise it slightly to force encoding of question marks
val escaper = new PercentEscaper("-._~!$'()*,;&=@:+/", false)
def formatResourceForClient(resource: Resource, topLevelParent: Option[String]): Resource = resource match {
case basic: BasicResource =>
basic.copy(
uri = escaper.escape(basic.uri),
display = basic.display.orElse(Some(formatResourceUriForClient(resource.uri, topLevelParent))),
children = basic.children.map(child => child.copy(
uri = escaper.escape(child.uri),
display = child.display.orElse(Some(formatResourceUriForClient(child.uri, topLevelParent)))
)),
parents = basic.parents.map(parent => parent.copy(
uri = escaper.escape(parent.uri),
display = parent.display.orElse(Some(formatResourceUriForClient(parent.uri, topLevelParent)))
))
)
case doc: DocumentResource =>
doc.copy(
uri = escaper.escape(doc.uri),
display = doc.display.orElse(Some(formatResourceUriForClient(resource.uri, topLevelParent))),
children = doc.children.map(child => child.copy(
uri = escaper.escape(child.uri),
display = child.display.orElse(Some(formatResourceUriForClient(child.uri, topLevelParent)))
)),
parents = doc.parents.map(parent => parent.copy(
uri = escaper.escape(parent.uri),
display = parent.display.orElse(Some(formatResourceUriForClient(parent.uri, topLevelParent)))
))
)
case email: EmailResource =>
email.copy(
uri = escaper.escape(email.uri),
display = email.display.orElse(Some(formatResourceUriForClient(resource.uri, topLevelParent))),
children = email.children.map(child => child.copy(
uri = escaper.escape(child.uri),
display = child.display.orElse(Some(formatResourceUriForClient(child.uri, topLevelParent)))
)),
parents = email.parents.map(parent => parent.copy(
uri = escaper.escape(parent.uri),
display = parent.display.orElse(Some(formatResourceUriForClient(parent.uri, topLevelParent)))
))
)
}
def formatResourceUriForClient(uri: String, topLevelParent: Option[String]): String = {
topLevelParent match {
case Some(parent) if uri.startsWith(parent) =>
uri
case Some(parent) =>
(parent +: uri.split('/').drop(1)).mkString("/")
case None =>
uri
}
}
}