Skip to content

Commit

Permalink
Add method & endpoint to update custom file metadata (#4775)
Browse files Browse the repository at this point in the history
  • Loading branch information
olivergrabinski authored Mar 4, 2024
1 parent 32c61af commit 18fc6c4
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ final class Files(
} yield res
}.span("updateFile")

def updateMetadata(
id: FileId,
rev: Int,
metadata: FileCustomMetadata
)(implicit caller: Caller): IO[FileResource] = {
for {
(iri, _) <- id.expandIri(fetchContext.onModify)
res <- eval(UpdateFileCustomMetadata(iri, id.project, metadata, rev, caller.subject))
} yield res
}.span("updateFileMetadata")

/**
* Update a new file linking it from an existing file in a storage
*
Expand Down Expand Up @@ -570,6 +581,11 @@ object Files {
def updatedAttributes(e: FileAttributesUpdated): Option[FileState] = state.map { s =>
s.copy(rev = e.rev, attributes = s.attributes.copy( mediaType = e.mediaType, bytes = e.bytes, digest = e.digest), updatedAt = e.instant, updatedBy = e.subject)
}

def updatedCustomMetadata(e: FileCustomMetadataUpdated): Option[FileState] = state.map { s =>
val newAttributes = FileAttributes.setCustomMetadata(s.attributes, e.metadata)
s.copy(rev = e.rev, attributes = newAttributes, updatedAt = e.instant, updatedBy = e.subject)
}

def tagAdded(e: FileTagAdded): Option[FileState] = state.map { s =>
s.copy(rev = e.rev, tags = s.tags + (e.tag -> e.targetRev), updatedAt = e.instant, updatedBy = e.subject)
Expand All @@ -589,13 +605,14 @@ object Files {
}

event match {
case e: FileCreated => created(e)
case e: FileUpdated => updated(e)
case e: FileAttributesUpdated => updatedAttributes(e)
case e: FileTagAdded => tagAdded(e)
case e: FileTagDeleted => tagDeleted(e)
case e: FileDeprecated => deprecated(e)
case e: FileUndeprecated => undeprecated(e)
case e: FileCreated => created(e)
case e: FileUpdated => updated(e)
case e: FileAttributesUpdated => updatedAttributes(e)
case e: FileCustomMetadataUpdated => updatedCustomMetadata(e)
case e: FileTagAdded => tagAdded(e)
case e: FileTagDeleted => tagDeleted(e)
case e: FileDeprecated => deprecated(e)
case e: FileUndeprecated => undeprecated(e)
}
}

Expand Down Expand Up @@ -633,6 +650,16 @@ object Files {
// format: on
}

def updateCustomMetadata(c: UpdateFileCustomMetadata) = state match {
case None => IO.raiseError(FileNotFound(c.id, c.project))
case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev))
case Some(s) =>
clock.realTimeInstant
.map(
FileCustomMetadataUpdated(c.id, c.project, s.storage, s.storageType, c.metadata, s.rev + 1, _, c.subject)
)
}

def tag(c: TagFile) = state match {
case None => IO.raiseError(FileNotFound(c.id, c.project))
case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev))
Expand Down Expand Up @@ -671,13 +698,14 @@ object Files {
}

cmd match {
case c: CreateFile => create(c)
case c: UpdateFile => update(c)
case c: UpdateFileAttributes => updateAttributes(c)
case c: TagFile => tag(c)
case c: DeleteFileTag => deleteTag(c)
case c: DeprecateFile => deprecate(c)
case c: UndeprecateFile => undeprecate(c)
case c: CreateFile => create(c)
case c: UpdateFile => update(c)
case c: UpdateFileAttributes => updateAttributes(c)
case c: UpdateFileCustomMetadata => updateCustomMetadata(c)
case c: TagFile => tag(c)
case c: DeleteFileTag => deleteTag(c)
case c: DeprecateFile => deprecate(c)
case c: UndeprecateFile => undeprecate(c)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ object FileAttributes {
)
}

/** Set the metadata of the provided [[FileAttributes]] to the metadata provided in [[FileCustomMetadata]] */
def setCustomMetadata(attr: FileAttributes, newCustomMetadata: FileCustomMetadata): FileAttributes =
attr.copy(
keywords = newCustomMetadata.keywords.getOrElse(Map.empty),
description = newCustomMetadata.description,
name = newCustomMetadata.name
)

/**
* Enumeration of all possible inputs that generated the file attributes
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ object FileCommand {
tag: Option[UserTag]
) extends FileCommand

/**
* Command to update the custom metadata of a file
*
* @param id
* the file identifier
* @param project
* the project the file belongs to
* @param metadata
* the custom metadata to update
* @param rev
* the last known revision of the file
* @param subject
* the identity associated to this command
*/
final case class UpdateFileCustomMetadata(
id: Iri,
project: ProjectRef,
metadata: FileCustomMetadata,
rev: Int,
subject: Subject
) extends FileCommand

/**
* Command to update an asynchronously computed file attributes. This command gets issued when linking a file using a
* ''RemoteDiskStorage''. Since the attributes cannot be computed synchronously, ''NotComputedDigest'' and wrong size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model
import akka.http.scaladsl.model.ContentType
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileAttributes.FileAttributesOrigin
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{contexts, nxvFile, Files}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.ShowFileLocation
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageType
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.ContextValue
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.ShowFileLocation
import ch.epfl.bluebrain.nexus.delta.sdk.circe.JsonObjOps
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.IriEncoder
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.model.metrics.EventMetric._
Expand Down Expand Up @@ -130,6 +130,37 @@ object FileEvent {
tag: Option[UserTag]
) extends FileEvent

/**
* Event for the modification of the custom metadata of a file
*
* @param id
* the file identifier
* @param project
* the project the file belongs to
* @param storage
* the reference to the remote storage used
* @param storageType
* the type of storage
* @param metadata
* the new custom metadata
* @param rev
* the last known revision of the file
* @param instant
* the instant this event was created
* @param subject
* the subject which created this event
*/
final case class FileCustomMetadataUpdated(
id: Iri,
project: ProjectRef,
storage: ResourceRef.Revision,
storageType: StorageType,
metadata: FileCustomMetadata,
rev: Int,
instant: Instant,
subject: Subject
) extends FileEvent

/**
* Event for the modification of an asynchronously computed file attributes. This event gets recorded when linking a
* file using a ''RemoteDiskStorage''. Since the attributes cannot be computed synchronously, ''NotComputedDigest''
Expand Down Expand Up @@ -322,13 +353,14 @@ object FileEvent {
ProjectScopedMetric.from(
event,
event match {
case _: FileCreated => Created
case _: FileUpdated => Updated
case _: FileAttributesUpdated => Updated
case _: FileTagAdded => Tagged
case _: FileTagDeleted => TagDeleted
case _: FileDeprecated => Deprecated
case _: FileUndeprecated => Undeprecated
case _: FileCreated => Created
case _: FileUpdated => Updated
case _: FileAttributesUpdated => Updated
case _: FileCustomMetadataUpdated => Updated
case _: FileTagAdded => Tagged
case _: FileTagDeleted => TagDeleted
case _: FileDeprecated => Deprecated
case _: FileUndeprecated => Undeprecated
},
event.id,
Set(nxvFile),
Expand Down Expand Up @@ -466,6 +498,8 @@ object FileEvent {
fau.mediaType,
Some(FileAttributesOrigin.Storage)
)
case fcmu: FileCustomMetadataUpdated =>
FileExtraFields(fcmu.storage.iri, fcmu.storageType, None, None, None, None)
case fta: FileTagAdded =>
FileExtraFields(fta.storage.iri, fta.storageType, None, None, None, None)
case ftd: FileTagDeleted =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,7 @@ object FileRejection {
final case class InvalidMultipartFieldName(id: Iri)
extends FileRejection(s"File '$id' payload a Multipart/Form-Data without a 'file' part.")

/**
* Rejection returned when attempting to create/update a file with a Multipart/Form-Data payload that contains
* invalid metadata
*/
final case class InvalidCustomMetadata(err: String)
extends FileRejection(s"File payload contained metadata which could not be parsed: $err")
final case object EmptyCustomMetadata extends FileRejection(s"No metadata was provided")

/**
* Rejection returned when attempting to create/update a file with a Multipart/Form-Data payload that does not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,29 @@ final class FilesRoutes(
)
},
// Update a file
(extractRequestEntity & extractFileMetadata) { (entity, metadata) =>
emit(
files
.update(fileId, storage, rev, entity, tag, metadata)
.index(mode)
.attemptNarrow[FileRejection]
)
(requestEntityPresent & extractRequestEntity & extractFileMetadata) {
(entity, metadata) =>
emit(
files
.update(fileId, storage, rev, entity, tag, metadata)
.index(mode)
.attemptNarrow[FileRejection]
)
},
// Update custom metadata
(requestEntityEmpty & extractFileMetadata & authorizeFor(project, Write)) {
case Some(FileCustomMetadata.empty) =>
emit(
IO.raiseError[FileResource](EmptyCustomMetadata).attemptNarrow[FileRejection]
)
case Some(metadata) =>
emit(
files
.updateMetadata(fileId, rev, metadata)
.index(mode)
.attemptNarrow[FileRejection]
)
case None => reject
}
)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "https://bluebrain.github.io/nexus/vocabulary/file",
"project": "myorg/myproj",
"storage": "https://bluebrain.github.io/nexus/vocabulary/disk-storage?rev=1",
"storageType": "DiskStorage",
"metadata": {
"name": "A name",
"description": "A description",
"keywords": {
"key": "value"
}
},
"rev": 3,
"instant": "1970-01-01T00:00:00Z",
"subject": {
"subject": "username",
"realm": "myrealm",
"@type": "User"
},
"@type": "FileCustomMetadataUpdated"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"@context": [
"https://bluebrain.github.io/nexus/contexts/metadata.json",
"https://bluebrain.github.io/nexus/contexts/files.json"
],
"@type": "FileCustomMetadataUpdated",
"metadata": {
"name": "A name",
"description": "A description",
"keywords": {
"key": "value"
}
},
"_fileId": "https://bluebrain.github.io/nexus/vocabulary/file",
"_instant": "1970-01-01T00:00:00Z",
"_project": "http://localhost/v1/projects/myorg/myproj",
"_resourceId": "https://bluebrain.github.io/nexus/vocabulary/file",
"_rev": 3,
"_subject": "http://localhost/v1/realms/myrealm/users/username"
}
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,58 @@ class FilesSpec(fixture: RemoteStorageClientFixtures)
}
}

"updating the custom metadata of a file" should {

"succeed" in {
val id = fileId(genString())
val metadata = genCustomMetadata()
val file = entity(genString())

files.create(id, Some(diskId), file, None, None).accepted
files.updateMetadata(id, 1, metadata).accepted

files.fetch(id).accepted.rev shouldEqual 2
assertCorrectCustomMetadata(id, metadata)
}

"reject if the wrong revision is specified" in {
val id = fileId(genString())
val metadata = genCustomMetadata()
val file = entity(genString())

files.create(id, Some(diskId), file, None, None).accepted
files
.updateMetadata(id, 2, metadata)
.rejected shouldEqual IncorrectRev(expected = 1, provided = 2)
}

"reject if file doesn't exists" in {
val nonExistentFile = fileIdIri(nxv + genString())

files
.updateMetadata(nonExistentFile, 1, genCustomMetadata())
.rejectedWith[FileNotFound]
}

"reject if project does not exist" in {
val nonexistentProject = ProjectRef(org, Label.unsafe(genString()))
val fileInNonexistentProject = FileId(genString(), nonexistentProject)

files
.updateMetadata(fileInNonexistentProject, 1, genCustomMetadata())
.rejectedWith[ProjectNotFound]
}

"reject if project is deprecated" in {
val fileInDeprecatedProject = FileId(genString(), deprecatedProject.ref)

files
.updateMetadata(fileInDeprecatedProject, 1, genCustomMetadata())
.rejectedWith[ProjectIsDeprecated]
}

}

"updating remote disk file attributes" should {

"reject if digest is already computed" in {
Expand Down
Loading

0 comments on commit 18fc6c4

Please sign in to comment.