Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an endpoint to undeprecate files #4461

Merged
merged 25 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9250078
Add FileUndeprecated event
olivergrabinski Nov 2, 2023
ab7c5f0
Add method to undeprecate files
olivergrabinski Nov 3, 2023
3e6a847
Improve `.accepted`
olivergrabinski Nov 3, 2023
bfe30b6
Improve FileSpec undeprecation tests
olivergrabinski Nov 3, 2023
e6dc8b8
Make FileRoutesSpec independent w.r.t. permissions
olivergrabinski Nov 3, 2023
216f931
Use non-confusing names
olivergrabinski Nov 3, 2023
c89bf3d
Add undeprecate endpoint and test it
olivergrabinski Nov 3, 2023
2d56da3
Make FilesRoutesSpec tests independent
olivergrabinski Nov 3, 2023
1f93b34
Fix user naming
olivergrabinski Nov 3, 2023
e40e5ed
Add integration tests
olivergrabinski Nov 3, 2023
f5cc6fb
scalafmt
olivergrabinski Nov 6, 2023
ce5a825
Add tests to FilesStmSpec for undeprecation
olivergrabinski Nov 6, 2023
5cc9786
Update version of storage used in tests
olivergrabinski Nov 6, 2023
9275fb7
Move `fileMetadata`
olivergrabinski Nov 6, 2023
d3578d8
Merge branch 'master' into undeprecate-files
olivergrabinski Nov 6, 2023
f53d3c4
Fix RemoteStorageClientSpec
olivergrabinski Nov 6, 2023
02a60ad
Fix FileRoutesSpec
olivergrabinski Nov 6, 2023
1b2b7e6
scalafmt
olivergrabinski Nov 6, 2023
89148d5
scalafmt
olivergrabinski Nov 6, 2023
d6a5256
Merge branch 'master' into undeprecate-files
shinyhappydan Nov 6, 2023
0459fa4
Update integration test
olivergrabinski Nov 6, 2023
ac87f61
User other `fail` method
olivergrabinski Nov 6, 2023
1396655
Inline json errors
olivergrabinski Nov 6, 2023
ab7bc75
Add dox
olivergrabinski Nov 8, 2023
38c3f3f
Merge branch 'master' into undeprecate-files
shinyhappydan Nov 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.Proje
import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesConfig, ResourcesImpl, ValidateResource}
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema
import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef}
import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap
Expand All @@ -44,16 +44,16 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues
private val uuid = UUID.randomUUID()
implicit private val uuidF: UUIDF = UUIDF.fixed(uuid)

private val reader = alice
private val writer = bob
private val reader = User("reader", realm)
private val writer = User("writer", realm)

implicit private val callerAlice: Caller =
Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm)))
implicit private val callerBob: Caller =
Caller(bob, Set(bob, Anonymous, Authenticated(realm), Group("group", realm)))
implicit private val callerReader: Caller =
Caller(reader, Set(reader, Anonymous, Authenticated(realm), Group("group", realm)))
implicit private val callerWriter: Caller =
Caller(writer, Set(writer, Anonymous, Authenticated(realm), Group("group", realm)))

private val asReader = addCredentials(OAuth2BearerToken("alice"))
private val asWriter = addCredentials(OAuth2BearerToken("bob"))
private val asReader = addCredentials(OAuth2BearerToken("reader"))
private val asWriter = addCredentials(OAuth2BearerToken("writer"))

private val am = ApiMappings("nxv" -> nxv.base, "Person" -> schema.Person)
private val projBase = nxv.base
Expand Down Expand Up @@ -114,7 +114,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues
(
Route.seal(
ResourcesRoutes(
IdentitiesDummy(callerAlice, callerBob),
IdentitiesDummy(callerReader, callerWriter),
aclCheck,
resources,
DeltaSchemeDirectives(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,26 @@ final class Files(
} yield res
}.span("deprecateFile")

/**
* Undeprecate an existing file
*
* @param id
* the file identifier to expand as the iri of the file
* @param projectRef
* the project where the file belongs
* @param rev
* the current revision of the file
*/
def undeprecate(
id: FileId,
rev: Int
)(implicit subject: Subject): IO[FileResource] = {
for {
(iri, _) <- id.expandIri(fetchContext.onModify)
res <- eval(UndeprecateFile(iri, id.project, rev, subject))
} yield res
}.span("undeprecateFile")

/**
* Fetch the last version of a file content
*
Expand Down Expand Up @@ -604,13 +624,18 @@ object Files {
s.copy(rev = e.rev, deprecated = true, updatedAt = e.instant, updatedBy = e.subject)
}

def undeprecated(e: FileUndeprecated): Option[FileState] = state.map { s =>
s.copy(rev = e.rev, deprecated = false, updatedAt = e.instant, updatedBy = e.subject)
}

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)
}
}

Expand Down Expand Up @@ -678,13 +703,22 @@ object Files {
IOInstant.now.map(FileDeprecated(c.id, c.project, s.storage, s.storageType, s.rev + 1, _, c.subject))
}

def undeprecate(c: UndeprecateFile) = 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) if !s.deprecated => IO.raiseError(FileIsNotDeprecated(c.id))
case Some(s) =>
IOInstant.now.map(FileUndeprecated(c.id, c.project, s.storage, s.storageType, s.rev + 1, _, c.subject))
}

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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,18 @@ object FileCommand {
* the identity associated to this command
*/
final case class DeprecateFile(id: Iri, project: ProjectRef, rev: Int, subject: Subject) extends FileCommand

/**
* Command to undeprecate a file
*
* @param id
* the file identifier
* @param project
* the project the file belongs to
* @param rev
* the last known revision of the file
* @param subject
* the identity associated to this command
*/
final case class UndeprecateFile(id: Iri, project: ProjectRef, rev: Int, subject: Subject) extends FileCommand
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,34 @@ object FileEvent {
subject: Subject
) extends FileEvent

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

@nowarn("cat=unused")
val serializer: Serializer[Iri, FileEvent] = {
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Database._
Expand Down Expand Up @@ -291,6 +319,7 @@ object FileEvent {
case _: FileTagAdded => Tagged
case _: FileTagDeleted => TagDeleted
case _: FileDeprecated => Deprecated
case _: FileUndeprecated => Undeprecated
},
event.id,
Set(nxvFile),
Expand Down Expand Up @@ -427,6 +456,8 @@ object FileEvent {
FileExtraFields(ftd.storage.iri, ftd.storageType, None, None, None, None)
case fd: FileDeprecated =>
FileExtraFields(fd.storage.iri, fd.storageType, None, None, None, None)
case fud: FileUndeprecated =>
FileExtraFields(fud.storage.iri, fud.storageType, None, None, None, None)
}

implicit val fileExtraFieldsEncoder: Encoder.AsObject[FileExtraFields] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ object FileRejection {
*/
final case class FileIsDeprecated(id: Iri) extends FileRejection(s"File '$id' is deprecated.")

/**
* Rejection returned when attempting to undeprecate a file that is already deprecated.
*
* @param id
* the file identifier
*/
final case class FileIsNotDeprecated(id: Iri) extends FileRejection(s"File '$id' is not deprecated.")

/**
* Rejection returned when attempting to link a file without providing a filename or a path that ends with a
* filename.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ final class FilesRoutes(
},
// Create a file with id segment
extractRequestEntity { entity =>
println(s"DTBDTB or only here?")
emit(
Created,
files.create(fileId, storage, entity, tag).index(mode).attemptNarrow[FileRejection]
Expand Down Expand Up @@ -168,6 +167,7 @@ final class FilesRoutes(
)
}
},

// Fetch a file
(get & idSegmentRef(id)) { id =>
emitOrFusionRedirect(ref, id, fetch(FileId(id, ref)))
Expand Down Expand Up @@ -213,6 +213,17 @@ final class FilesRoutes(
}
)
}
},
(pathPrefix("undeprecate") & put & parameter("rev".as[Int])) { rev =>
authorizeFor(ref, Write).apply {
emit(
files
.undeprecate(fileId, rev)
.index(mode)
Comment on lines +220 to +221
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really an issue of your PR but we should be doing indexing within the Files here

.attemptNarrow[FileRejection]
.rejectOn[FileNotFound]
)
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id" : "https://bluebrain.github.io/nexus/vocabulary/file",
"project" : "myorg/myproj",
"storage": "https://bluebrain.github.io/nexus/vocabulary/disk-storage?rev=1",
"storageType": "DiskStorage",
"rev" : 6,
"instant" : "1970-01-01T00:00:00Z",
"subject" : {
"subject" : "username",
"realm" : "myrealm",
"@type" : "User"
},
"@type" : "FileUndeprecated"
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"@context": [
"https://bluebrain.github.io/nexus/contexts/metadata.json",
"https://bluebrain.github.io/nexus/contexts/files.json"
],
"@type": "FileUndeprecated",
"_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": 6,
"_subject": "http://localhost/v1/realms/myrealm/users/username"
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import monix.bio.Task
import org.scalatest.Suite

import java.nio.file.{Files => JavaFiles}
import java.util.UUID
import java.util.{Base64, UUID}

trait FileFixtures extends EitherValues with BIOValues {

Expand All @@ -41,6 +41,7 @@ trait FileFixtures extends EitherValues with BIOValues {
val fileTagged = nxv + "fileTagged"
val fileTagged2 = nxv + "fileTagged2"
val file1Encoded = UrlUtils.encode(file1.toString)
val encodeId = (id: String) => UrlUtils.encode((nxv + id).toString)
val generatedId = project.base.iri / uuid.toString
val generatedId2 = project.base.iri / uuid2.toString

Expand Down Expand Up @@ -82,4 +83,9 @@ trait FileFixtures extends EitherValues with BIOValues {
Multipart.FormData.BodyPart("file", HttpEntity(`text/plain(UTF-8)`, "0" * size), Map("filename" -> filename))
)
.toEntity()

def base64encode(input: String) = {
val encodedBytes = Base64.getEncoder.encode(input.getBytes("UTF-8"))
new String(encodedBytes, "UTF-8")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture
import ch.epfl.bluebrain.nexus.testkit.remotestorage.RemoteStorageDocker
import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec
import monix.execution.Scheduler
import org.scalatest.DoNotDiscover
import org.scalatest.{Assertion, DoNotDiscover}
import org.scalatest.concurrent.Eventually

import java.net.URLDecoder
Expand Down Expand Up @@ -490,6 +490,44 @@ class FilesSpec(docker: RemoteStorageDocker)

}

"undeprecating a file" should {

"succeed" in {
givenADeprecatedFile { id =>
files.undeprecate(id, 2).accepted.deprecated shouldEqual false
assertActive(id)
}
}

"reject if file doesn't exists" in {
files.undeprecate(fileId("404"), 1).rejectedWith[FileNotFound]
}

"reject if file is not deprecated" in {
givenAFile { id =>
files.undeprecate(id, 1).assertRejectedWith[FileIsNotDeprecated]
assertRemainsActive(id)
}
}

"reject if the revision passed is incorrect" in {
givenADeprecatedFile { id =>
files.undeprecate(id, 3).assertRejectedEquals(IncorrectRev(3, 2))
assertRemainsDeprecated(id)
}
}

"reject if project does not exist" in {
val wrongProject = ProjectRef(org, Label.unsafe("other"))
files.deprecate(FileId(nxv + "id", wrongProject), 1).rejectedWith[ProjectContextRejection]
}

"reject if project is deprecated" in {
files.undeprecate(FileId(nxv + "id", deprecatedProject.ref), 2).rejectedWith[ProjectContextRejection]
}

}

Comment on lines +493 to +530
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice test

"fetching a file" should {
val resourceRev1 = mkResource(file1, projectRef, diskRev, attributes("myfile.txt"))
val resourceRev4 = mkResource(file1, projectRef, diskRev, attributes(), rev = 4)
Expand Down Expand Up @@ -583,6 +621,29 @@ class FilesSpec(docker: RemoteStorageDocker)
}

}

def givenAFile(assertion: FileId => Assertion): Assertion = {
val filename = genString()
val id = fileId(filename)
files.create(id, Some(diskId), randomEntity(filename, 1), None).accepted
files.fetch(id).accepted
assertion(id)
}

def givenADeprecatedFile(assertion: FileId => Assertion): Assertion = {
givenAFile { id =>
files.deprecate(id, 1).accepted
files.fetch(id).accepted.deprecated shouldEqual true
assertion(id)
}
}

def assertRemainsDeprecated(id: FileId): Assertion =
files.fetch(id).accepted.deprecated shouldEqual true
def assertActive(id: FileId): Assertion =
files.fetch(id).accepted.deprecated shouldEqual false
def assertRemainsActive(id: FileId): Assertion =
assertActive(id)
}

}
Loading