Skip to content

Commit

Permalink
Add custom header for file metadata (#4764)
Browse files Browse the repository at this point in the history
  • Loading branch information
olivergrabinski authored Mar 4, 2024
1 parent 28052e7 commit 32c61af
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ final class Files(
storageId: Option[IdSegment],
projectRef: ProjectRef,
entity: HttpEntity,
tag: Option[UserTag]
tag: Option[UserTag],
metadata: Option[FileCustomMetadata]
)(implicit caller: Caller): IO[FileResource] = {
for {
pc <- fetchContext.onCreate(projectRef)
iri <- generateId(pc)
_ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag))
(storageRef, storage) <- fetchAndValidateActiveStorage(storageId, projectRef, pc)
attributes <- saveFileToStorage(iri, entity, storage)
attributes <- saveFileToStorage(iri, entity, storage, metadata)
res <- eval(CreateFile(iri, projectRef, storageRef, storage.tpe, attributes, caller.subject, tag))
} yield res
}.span("createFile")
Expand All @@ -117,13 +118,14 @@ final class Files(
id: FileId,
storageId: Option[IdSegment],
entity: HttpEntity,
tag: Option[UserTag]
tag: Option[UserTag],
metadata: Option[FileCustomMetadata]
)(implicit caller: Caller): IO[FileResource] = {
for {
(iri, pc) <- id.expandIri(fetchContext.onCreate)
_ <- test(CreateFile(iri, id.project, testStorageRef, testStorageType, testAttributes, caller.subject, tag))
(storageRef, storage) <- fetchAndValidateActiveStorage(storageId, id.project, pc)
metadata <- saveFileToStorage(iri, entity, storage)
metadata <- saveFileToStorage(iri, entity, storage, metadata)
res <- eval(CreateFile(iri, id.project, storageRef, storage.tpe, metadata, caller.subject, tag))
} yield res
}.span("createFile")
Expand Down Expand Up @@ -208,13 +210,14 @@ final class Files(
storageId: Option[IdSegment],
rev: Int,
entity: HttpEntity,
tag: Option[UserTag]
tag: Option[UserTag],
metadata: Option[FileCustomMetadata]
)(implicit caller: Caller): IO[FileResource] = {
for {
(iri, pc) <- id.expandIri(fetchContext.onModify)
_ <- test(UpdateFile(iri, id.project, testStorageRef, testStorageType, testAttributes, rev, caller.subject, tag))
(storageRef, storage) <- fetchAndValidateActiveStorage(storageId, id.project, pc)
attributes <- saveFileToStorage(iri, entity, storage)
attributes <- saveFileToStorage(iri, entity, storage, metadata)
res <- eval(UpdateFile(iri, id.project, storageRef, storage.tpe, attributes, rev, caller.subject, tag))
} yield res
}.span("updateFile")
Expand Down Expand Up @@ -468,11 +471,12 @@ final class Files(
private def saveFileToStorage(
iri: Iri,
entity: HttpEntity,
storage: Storage
storage: Storage,
fileMetadata: Option[FileCustomMetadata]
): IO[FileAttributes] =
for {
info <- extractFormData(iri, storage, entity)
description = FileDescription.from(info)
description = FileDescription.from(info, fileMetadata)
storageMetadata <- saveFile(iri, storage, description, info.contents)
} yield FileAttributes.from(description, storageMetadata)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ 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, InvalidCustomMetadata, InvalidMultipartFieldName, WrappedAkkaRejection}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileCustomMetadata, FileRejection}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{FileTooLarge, 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 scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
Expand Down Expand Up @@ -49,9 +46,6 @@ sealed trait FormDataExtractor {

case class UploadedFileInformation(
filename: String,
keywords: Map[Label, String],
description: Option[String],
name: Option[String],
suppliedContentType: ContentType,
contents: BodyPartEntity
)
Expand Down Expand Up @@ -140,36 +134,17 @@ object FormDataExtractor {
val filename = part.filename.getOrElse("file")
val contentType = detectContentType(filename, part.entity.contentType)

val result = extractMetadata(part).map { md =>
Future(
UploadedFileInformation(
filename,
md.keywords.getOrElse(Map.empty),
md.description,
md.name,
contentType,
part.entity
).some
}

Future.fromTry(result.toTry)
)
case part =>
part.entity.discardBytes().future.as(None)
}

private def extractMetadata(
part: Multipart.FormData.BodyPart
): Either[FileRejection, FileCustomMetadata] = {
val metadata = part.dispositionParams.get("metadata").filter(_.nonEmpty)
metadata match {
case Some(value) =>
parser
.parse(value)
.flatMap(_.as[FileCustomMetadata])
.leftMap(err => InvalidCustomMetadata(err.getMessage))
case None => Right(FileCustomMetadata(None, None, None))
}
}

private def detectContentType(filename: String, contentTypeFromRequest: ContentType) = {
val bodyDefinedContentType = Option.when(contentTypeFromRequest != defaultContentType)(contentTypeFromRequest)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model

import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
import io.circe.Decoder
import io.circe.generic.semiauto.deriveDecoder
import io.circe.Codec
import io.circe.generic.semiauto.deriveCodec

/**
* Custom metadata for a file that can be specified by the user.
Expand All @@ -15,8 +15,8 @@ case class FileCustomMetadata(

object FileCustomMetadata {

implicit val fileUploadMetadataDecoder: Decoder[FileCustomMetadata] =
deriveDecoder[FileCustomMetadata]
implicit val fileUploadMetadataDecoder: Codec[FileCustomMetadata] =
deriveCodec[FileCustomMetadata]

val empty: FileCustomMetadata = FileCustomMetadata(None, None, None)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ object FileDescription {
).some
)

def from(info: UploadedFileInformation): FileDescription =
def from(info: UploadedFileInformation, metadata: Option[FileCustomMetadata]): FileDescription = {
val md = metadata.getOrElse(FileCustomMetadata.empty)
FileDescription(
info.filename,
Some(info.suppliedContentType),
FileCustomMetadata(
info.name,
info.description,
Some(info.keywords)
md.name,
md.description,
md.keywords
).some
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.http.scaladsl.model.StatusCodes.Created
import akka.http.scaladsl.model.Uri.Path
import akka.http.scaladsl.model.headers.Accept
import akka.http.scaladsl.model.{ContentType, MediaRange}
import akka.http.scaladsl.server.Directives.{optionalHeaderValueByName, provide, reject}
import akka.http.scaladsl.server._
import cats.effect.IO
import cats.syntax.all._
Expand All @@ -29,9 +30,9 @@ import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
import io.circe.Decoder
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredDecoder
import io.circe.{parser, Decoder}
import kamon.instrumentation.akka.http.TracingDirectives.operationName

import scala.annotation.nowarn
Expand Down Expand Up @@ -97,10 +98,10 @@ final class FilesRoutes(
)
},
// Create a file without id segment
extractRequestEntity { entity =>
(extractRequestEntity & extractFileMetadata) { (entity, metadata) =>
emit(
Created,
files.create(storage, project, entity, tag).index(mode).attemptNarrow[FileRejection]
files.create(storage, project, entity, tag, metadata).index(mode).attemptNarrow[FileRejection]
)
}
)
Expand Down Expand Up @@ -137,10 +138,10 @@ final class FilesRoutes(
)
},
// Update a file
extractRequestEntity { entity =>
(extractRequestEntity & extractFileMetadata) { (entity, metadata) =>
emit(
files
.update(fileId, storage, rev, entity, tag)
.update(fileId, storage, rev, entity, tag, metadata)
.index(mode)
.attemptNarrow[FileRejection]
)
Expand All @@ -163,11 +164,11 @@ final class FilesRoutes(
)
},
// Create a file with id segment
extractRequestEntity { entity =>
(extractRequestEntity & extractFileMetadata) { (entity, metadata) =>
emit(
Created,
files
.create(fileId, storage, entity, tag)
.create(fileId, storage, entity, tag, metadata)
.index(mode)
.attemptNarrow[FileRejection]
)
Expand Down Expand Up @@ -290,6 +291,21 @@ object FilesRoutes {
fusionConfig: FusionConfig
): Route = new FilesRoutes(identities, aclCheck, files, schemeDirectives, index).routes

/**
* An akka directive to extract the optional [[FileCustomMetadata]] from a request. This metadata is extracted from
* the `x-nxs-file-metadata` header. In case the decoding fails, a [[MalformedHeaderRejection]] is returned.
*/
def extractFileMetadata: Directive1[Option[FileCustomMetadata]] =
optionalHeaderValueByName("x-nxs-file-metadata").flatMap {
case Some(metadata) =>
val md = parser.parse(metadata).flatMap(_.as[FileCustomMetadata])
md match {
case Right(value) => provide(Some(value))
case Left(err) => reject(MalformedHeaderRejection("x-nxs-file-metadata", err.getMessage))
}
case None => provide(Some(FileCustomMetadata.empty))
}

final case class LinkFileRequest(
path: Path,
filename: Option[String],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cats.effect.unsafe.implicits.global
import cats.effect.{IO, Ref}
import ch.epfl.bluebrain.nexus.delta.kernel.utils.{UUIDF, UrlUtils}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.generators.FileGen
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileAttributes
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileCustomMetadata}
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv
import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings
Expand Down Expand Up @@ -58,6 +58,9 @@ trait FileFixtures extends Generators {

def genKeywords(): Map[Label, String] = Map(Label.unsafe(genString()) -> genString())

def genCustomMetadata(): FileCustomMetadata =
FileCustomMetadata(Some(genString()), Some(genString()), Some(genKeywords()))

def entity(filename: String = "file.txt"): MessageEntity =
Multipart
.FormData(
Expand Down
Loading

0 comments on commit 32c61af

Please sign in to comment.