From 2b47d562b906ae292d084001a7d73c1649d33161 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:37:32 +0100 Subject: [PATCH] add metadata field --- .../storage/files/FormDataExtractor.scala | 31 ++++++++++++++++--- .../storage/files/model/FileRejection.scala | 7 +++++ .../storage/files/FormDataExtractorSpec.scala | 15 ++++++--- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractor.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractor.scala index 7bd8f73e4f..78bcc396b6 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractor.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractor.scala @@ -13,10 +13,12 @@ import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.error.NotARejection import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.FileUtils -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{FileTooLarge, InvalidKeywords, InvalidMultipartFieldName, WrappedAkkaRejection} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{FileTooLarge, InvalidFileMetadata, InvalidKeywords, InvalidMultipartFieldName, WrappedAkkaRejection} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label -import io.circe.parser +import io.circe.generic.semiauto.deriveDecoder +import io.circe.{parser, Decoder} import scala.concurrent.{ExecutionContext, Future} import scala.util.Try @@ -138,13 +140,14 @@ object FormDataExtractor { case part if part.name == FileFieldName => val filename = part.filename.getOrElse("file") val contentType = detectContentType(filename, part.entity.contentType) - val description = part.dispositionParams.get("description").filter(_.nonEmpty) - val name = part.dispositionParams.get("descriptiveName").filter(_.nonEmpty) val result = for { keywords <- extractKeywords(part) + metadata <- extractMetadata(part) } yield { - Some(UploadedFileInformation(filename, keywords, description, name, contentType, part.entity)) + Some( + UploadedFileInformation(filename, keywords, metadata.description, metadata.name, contentType, part.entity) + ) } Future.fromTry(result.toTry) @@ -165,6 +168,24 @@ object FormDataExtractor { } } + private case class FileUploadMetadata(name: Option[String], description: Option[String]) + implicit private val fileUploadMetadataDecoder: Decoder[FileUploadMetadata] = + deriveDecoder[FileUploadMetadata] + + private def extractMetadata( + part: Multipart.FormData.BodyPart + ): Either[FileRejection, FileUploadMetadata] = { + val metadata = part.dispositionParams.get("metadata").filter(_.nonEmpty) + metadata match { + case Some(value) => + parser + .parse(value) + .flatMap(_.as[FileUploadMetadata]) + .leftMap(err => InvalidFileMetadata(err.getMessage)) + case None => Right(FileUploadMetadata(None, None)) + } + } + private def detectContentType(filename: String, contentTypeFromRequest: ContentType) = { val bodyDefinedContentType = Option.when(contentTypeFromRequest != defaultContentType)(contentTypeFromRequest) diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala index e2918e43e8..3df370b11b 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala @@ -148,6 +148,13 @@ object FileRejection { final case class InvalidKeywords(err: String) extends FileRejection(s"File payload contained keywords which could not be parsed: $err") + /** + * Rejection returned when attempting to create/update a file with a Multipart/Form-Data payload that contains + * invalid metadata + */ + final case class InvalidFileMetadata(err: String) + extends FileRejection(s"File payload contained metadata which could not be parsed: $err") + /** * Rejection returned when attempting to create/update a file with a Multipart/Form-Data payload that does not * contain a ''file'' fieldName diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractorSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractorSpec.scala index d734ccf3df..f91b343781 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractorSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractorSpec.scala @@ -10,7 +10,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.AkkaSou import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec -import io.circe.syntax.KeyOps +import io.circe.syntax.{EncoderOps, KeyOps} import io.circe.{Json, JsonObject} class FormDataExtractorSpec @@ -67,11 +67,16 @@ class FormDataExtractorSpec description: Option[String], name: Option[String] ): Map[String, String] = { + + val metadata = JsonObject( + "name" -> name.asJson, + "description" -> description.asJson + ).toJson + Map.from( filename.map("filename" -> _) ++ Option.when(keywords.nonEmpty)("keywords" -> JsonObject.fromMap(keywords).toJson.noSpaces) ++ - description.map("description" -> _) ++ - name.map("descriptiveName" -> _) + Option.when(!metadata.isEmpty())("metadata" -> metadata.noSpaces) ) } @@ -79,7 +84,7 @@ class FormDataExtractorSpec val entity = createEntity("file", NoContentType, Some("filename")) val UploadedFileInformation(filename, _, _, _, contentType, contents) = - extractor(iri, entity, 179, None).accepted + extractor(iri, entity, 200, None).accepted filename shouldEqual "filename" contentType shouldEqual `application/octet-stream` @@ -100,7 +105,7 @@ class FormDataExtractorSpec val entity = createEntity("file", NoContentType, Some("file.txt")) val UploadedFileInformation(filename, _, _, _, contentType, contents) = - extractor(iri, entity, 179, None).accepted + extractor(iri, entity, 200, None).accepted filename shouldEqual "file.txt" contentType shouldEqual `text/plain(UTF-8)` consume(contents.dataBytes) shouldEqual content