From a6114816445dd570b06eaabb274a0563145ae846 Mon Sep 17 00:00:00 2001 From: Simon Dumas Date: Fri, 9 Aug 2024 13:04:02 +0200 Subject: [PATCH] Fix delegation + add the ability to tag at creation --- .../delta/plugins/storage/files/Files.scala | 15 ++- .../files/model/FileDelegationRequest.scala | 25 ++++ .../storage/files/model/FileDescription.scala | 7 ++ .../files/routes/DelegateFilesRoutes.scala | 110 +++++++----------- .../nexus/tests/kg/files/S3StorageSpec.scala | 4 +- 5 files changed, 86 insertions(+), 75 deletions(-) create mode 100644 delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDelegationRequest.scala diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala index 2e47c1178a..3b255d3c76 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala @@ -15,7 +15,6 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileCommand._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileEvent._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._ -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.DelegateFilesRoutes.DelegationResponse import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.schemas.{files => fileSchema} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageRejection.{StorageFetchRejection, StorageIsDeprecated} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{DigestAlgorithm, Storage, StorageRejection, StorageType} @@ -181,17 +180,21 @@ final class Files( * @param storageId * the optional storage identifier to expand as the id of the storage. When None, the default storage is used */ - def delegate(projectRef: ProjectRef, description: FileDescription, storageId: Option[IdSegment])(implicit + def delegate( + projectRef: ProjectRef, + description: FileDescription, + storageId: Option[IdSegment], + tag: Option[UserTag] + )(implicit caller: Caller - ): IO[DelegationResponse] = { + ): IO[FileDelegationRequest] = { for { pc <- fetchContext.onCreate(projectRef) iri <- generateId(pc) - _ <- - test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag = None)) + _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag = tag)) (_, storage) <- fetchAndValidateActiveStorage(storageId, projectRef, pc) metadata <- fileOperations.delegate(storage, description.filename) - } yield DelegationResponse(metadata.bucket, iri, metadata.path, description.metadata, description.mediaType) + } yield FileDelegationRequest(storage.id, metadata.bucket, projectRef, iri, metadata.path, description, tag) }.span("delegate") /** diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDelegationRequest.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDelegationRequest.scala new file mode 100644 index 0000000000..84a36e186a --- /dev/null +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDelegationRequest.scala @@ -0,0 +1,25 @@ +package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model + +import akka.http.scaladsl.model.Uri +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import io.circe.Codec +import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ +import io.circe.generic.extras.Configuration +import io.circe.generic.extras.semiauto.deriveConfiguredCodec + +final case class FileDelegationRequest( + storageId: Iri, + bucket: String, + project: ProjectRef, + id: Iri, + path: Uri, + description: FileDescription, + tag: Option[UserTag] +) + +object FileDelegationRequest { + implicit private val config: Configuration = Configuration.default + implicit val codec: Codec[FileDelegationRequest] = deriveConfiguredCodec +} diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDescription.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDescription.scala index 87b70e7177..4eed1b7e70 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDescription.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileDescription.scala @@ -3,6 +3,10 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model import akka.http.scaladsl.model.ContentType import cats.implicits.catsSyntaxOptionId import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.UploadedFileInformation +import io.circe.Codec +import io.circe.generic.extras.Configuration +import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ +import io.circe.generic.extras.semiauto.deriveConfiguredCodec case class FileDescription( filename: String, @@ -39,4 +43,7 @@ object FileDescription { ) } + implicit private val config: Configuration = Configuration.default + implicit val fileDescriptionCodec: Codec[FileDescription] = deriveConfiguredCodec[FileDescription] + } diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala index 9105058265..192f16f717 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala @@ -1,35 +1,29 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes import akka.http.scaladsl.model.StatusCodes.{Created, OK} -import akka.http.scaladsl.model.{ContentType, Uri} import akka.http.scaladsl.server._ import cats.effect.IO import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._ -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.DelegateFilesRoutes._ +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FileUriDirectives._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{FileResource, Files} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.ShowFileLocation -import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering -import ch.epfl.bluebrain.nexus.delta.sdk.{IndexingAction, IndexingMode} import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.circe.{CirceMarshalling, CirceUnmarshalling} import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, ResponseToJsonLd} import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment} -import io.circe.generic.extras.Configuration -import io.circe.generic.extras.semiauto.deriveConfiguredDecoder -import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import io.circe.syntax.EncoderOps -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FileUriDirectives._ -import io.circe.{Decoder, Encoder, Json} -import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.jws.JWSPayloadHelper +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment} +import ch.epfl.bluebrain.nexus.delta.sdk.{IndexingAction, IndexingMode} import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import io.circe.Json +import io.circe.syntax.EncoderOps final class DelegateFilesRoutes( identities: Identities, @@ -50,73 +44,55 @@ final class DelegateFilesRoutes( baseUriPrefix(baseUri.prefix) { pathPrefix("delegate" / "files") { extractCaller { implicit caller => - projectRef { project => - concat( - pathPrefix("validate") { - (pathEndOrSingleSlash & post) { - storageParam { storageId => - entity(as[FileDescription]) { desc => - emit(OK, validateFileDetails(project, storageId, desc).attemptNarrow[FileRejection]) - } - } + concat( + (projectRef & pathPrefix("generate") & storageParam & tagParam & pathEndOrSingleSlash & post) { + (project, storageId, tag) => + entity(as[FileDescription]) { desc => + emit(OK, validateFileDetails(project, storageId, desc, tag).attemptNarrow[FileRejection]) } - }, - (pathEndOrSingleSlash & post) { - (storageParam & indexingMode) { (storageId, mode) => - entity(as[Json]) { jwsPayload => - emit( - Created, - linkDelegatedFile(jwsPayload, project, storageId, mode) - .attemptNarrow[FileRejection]: ResponseToJsonLd - ) - } - } - } - ) - } + }, + (pathPrefix("submit") & put & pathEndOrSingleSlash & entity(as[Json]) & indexingMode) { + (jwsPayload, mode) => + emit( + Created, + linkDelegatedFile(jwsPayload, mode).attemptNarrow[FileRejection]: ResponseToJsonLd + ) + } + ) } } } - private def validateFileDetails(project: ProjectRef, storageId: Option[IdSegment], desc: FileDescription)(implicit - c: Caller - ) = + private def validateFileDetails( + project: ProjectRef, + storageId: Option[IdSegment], + desc: FileDescription, + tag: Option[UserTag] + )(implicit c: Caller) = for { - delegationResp <- files.delegate(project, desc, storageId) - jwsPayload <- jwsPayloadHelper.sign(delegationResp.asJson) + delegationRequest <- files.delegate(project, desc, storageId, tag) + jwsPayload <- jwsPayloadHelper.sign(delegationRequest.asJson) } yield jwsPayload private def linkDelegatedFile( jwsPayload: Json, - project: ProjectRef, - storageId: Option[IdSegment], mode: IndexingMode )(implicit c: Caller): IO[FileResource] = for { - originalPayload <- jwsPayloadHelper.verify(jwsPayload) - delegationResponse <- IO.fromEither(originalPayload.as[DelegationResponse]) - request = FileLinkRequest(delegationResponse.path.path, delegationResponse.mediaType, delegationResponse.metadata) - fileResource <- files.linkFile(Some(delegationResponse.id), project, storageId, request, None) - _ <- index(project, fileResource, mode) + originalPayload <- jwsPayloadHelper.verify(jwsPayload) + delegationRequest <- IO.fromEither(originalPayload.as[FileDelegationRequest]) + linkRequest = FileLinkRequest( + delegationRequest.path.path, + delegationRequest.description.mediaType, + delegationRequest.description.metadata + ) + fileResource <- files.linkFile( + Some(delegationRequest.id), + delegationRequest.project, + Some(delegationRequest.storageId), + linkRequest, + delegationRequest.tag + ) + _ <- index(delegationRequest.project, fileResource, mode) } yield fileResource - -} - -object DelegateFilesRoutes { - - final case class DelegationResponse( - bucket: String, - id: Iri, - path: Uri, - metadata: Option[FileCustomMetadata], - mediaType: Option[ContentType] - ) - - object DelegationResponse { - implicit val enc: Encoder[DelegationResponse] = deriveEncoder - implicit val dec: Decoder[DelegationResponse] = deriveDecoder - } - - implicit private val config: Configuration = Configuration.default - implicit val dec: Decoder[FileDescription] = deriveConfiguredDecoder[FileDescription] } diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/files/S3StorageSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/files/S3StorageSpec.scala index 5236ed42a5..74a97b2240 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/files/S3StorageSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/files/S3StorageSpec.scala @@ -425,14 +425,14 @@ class S3StorageSpec extends StorageSpec { for { jwsPayload <- deltaClient - .postAndReturn[Json](s"/delegate/files/$projectRef/validate?storage=nxv:$storageId", payload, Coyote) { + .postAndReturn[Json](s"/delegate/files/$projectRef/generate?storage=nxv:$storageId", payload, Coyote) { expectOk } resp <- parseDelegationResponse(jwsPayload) DelegationResponse(id, path, returnedBucket) = resp _ = returnedBucket shouldEqual bucket _ <- uploadLogoFileToS3(path) - _ <- deltaClient.post[Json](s"/delegate/files/$projectRef?storage=nxv:$storageId", jwsPayload, Coyote) { + _ <- deltaClient.put[Json](s"/delegate/files/submit", jwsPayload, Coyote) { expectCreated } encodedId = UrlUtils.encode(id)