diff --git a/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/RetryStrategy.scala b/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/RetryStrategy.scala index 17cdc23d11..9303e0cf01 100644 --- a/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/RetryStrategy.scala +++ b/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/RetryStrategy.scala @@ -147,7 +147,7 @@ object RetryStrategy { RetryStrategy( config, (t: Throwable) => NonFatal(t), - (t: Throwable, d: RetryDetails) => logError[Throwable](logger, action)(t, d).toBIO + (t: Throwable, d: RetryDetails) => logError[Throwable](logger, action)(t, d).toBIOThrowable ) } diff --git a/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/effect/migration/MigrateEffectSyntax.scala b/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/effect/migration/MigrateEffectSyntax.scala index f9c2b1fdba..486d12813c 100644 --- a/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/effect/migration/MigrateEffectSyntax.scala +++ b/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/effect/migration/MigrateEffectSyntax.scala @@ -4,6 +4,9 @@ import cats.effect.IO import cats.~> import monix.bio.{IO => BIO, Task, UIO} import monix.execution.Scheduler.Implicits.global +import shapeless.=:!= + +import scala.annotation.nowarn import scala.reflect.ClassTag @@ -34,7 +37,20 @@ final class MonixBioToCatsIOEitherOps[E, A](private val io: BIO[E, A]) extends A } final class CatsIOToBioOps[A](private val io: IO[A]) extends AnyVal { - def toBIO[E <: Throwable](implicit E: ClassTag[E]): BIO[E, A] = + + /** + * Safe conversion between CE and Monix, forcing the user to specify a strict subtype of [[Throwable]]. If omitted, + * the compiler may infer [[Throwable]] and bypass any custom error handling. + */ + @SuppressWarnings(Array("UnusedMethodParameter")) + @nowarn + def toBIO[E <: Throwable](implicit E: ClassTag[E], ev: E =:!= Throwable): BIO[E, A] = + toBIOThrowable[E] + + /** + * Prefer [[toBIO]]. Only use this when we are sure there's no custom error handling logic. + */ + def toBIOThrowable[E <: Throwable](implicit E: ClassTag[E]): BIO[E, A] = BIO.from(io).mapErrorPartialWith { case E(e) => monix.bio.IO.raiseError(e) case other => BIO.terminate(other) diff --git a/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala b/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala index 9fed98b95f..57cf88625b 100644 --- a/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala +++ b/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala @@ -12,7 +12,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveReference.{Fil import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveRejection._ import ch.epfl.bluebrain.nexus.delta.plugins.archive.model._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.Files -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileId, FileRejection} import ch.epfl.bluebrain.nexus.delta.rdf.implicits._ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution @@ -27,7 +27,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.AnnotatedSource import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegmentRef, ResourceRepresentation} +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceRepresentation} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources import ch.epfl.bluebrain.nexus.delta.sdk.stream.StreamConverter import ch.epfl.bluebrain.nexus.delta.sdk.{AkkaSource, JsonLdValue, ResourceShifts} @@ -282,7 +282,7 @@ object ArchiveDownload { ArchiveDownload( aclCheck, shifts.fetch, - (id: ResourceRef, project: ProjectRef, caller: Caller) => files.fetchContent(IdSegmentRef(id), project)(caller), + (id: ResourceRef, project: ProjectRef, caller: Caller) => files.fetchContent(FileId(id, project))(caller), fileSelf ) diff --git a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/client/DeltaClient.scala b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/client/DeltaClient.scala index 4d660ace9e..93e8d5b958 100644 --- a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/client/DeltaClient.scala +++ b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/client/DeltaClient.scala @@ -95,7 +95,7 @@ object DeltaClient { override def projectStatistics(source: RemoteProjectSource): HttpResult[ProjectStatistics] = { for { - authToken <- authTokenProvider(credentials).toBIO + authToken <- authTokenProvider(credentials).toBIOThrowable request = Get( source.endpoint / "projects" / source.project.organization.value / source.project.project.value / "statistics" @@ -108,7 +108,7 @@ object DeltaClient { override def remaining(source: RemoteProjectSource, offset: Offset): HttpResult[RemainingElems] = { for { - authToken <- authTokenProvider(credentials).toBIO + authToken <- authTokenProvider(credentials).toBIOThrowable request = Get(elemAddress(source) / "remaining") .addHeader(accept) .addHeader(`Last-Event-ID`(offset.value.toString)) @@ -119,7 +119,7 @@ object DeltaClient { override def checkElems(source: RemoteProjectSource): HttpResult[Unit] = { for { - authToken <- authTokenProvider(credentials).toBIO + authToken <- authTokenProvider(credentials).toBIOThrowable result <- client(Head(elemAddress(source)).withCredentials(authToken)) { case resp if resp.status.isSuccess() => UIO.delay(resp.discardEntityBytes()) >> BIO.unit } @@ -134,7 +134,7 @@ object DeltaClient { def send(request: HttpRequest): Future[HttpResponse] = { (for { - authToken <- authTokenProvider(credentials).toBIO + authToken <- authTokenProvider(credentials).toBIOThrowable result <- client[HttpResponse](request.withCredentials(authToken))(BIO.pure(_)) } yield result).runToFuture } @@ -169,7 +169,7 @@ object DeltaClient { val resourceUrl = source.endpoint / "resources" / source.project.organization.value / source.project.project.value / "_" / id.toString for { - authToken <- authTokenProvider(credentials).toBIO + authToken <- authTokenProvider(credentials).toBIOThrowable req = Get( source.resourceTag.fold(resourceUrl)(t => resourceUrl.withQuery(Query("tag" -> t.value))) ).addHeader(Accept(RdfMediaTypes.`application/n-quads`)).withCredentials(authToken) diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala index 1f590164e2..a88d947fa7 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala @@ -2,7 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage import akka.actor import akka.actor.typed.ActorSystem -import cats.effect.{Clock, IO} +import cats.effect.{Clock, ContextShift, IO} import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.client.ElasticSearchClient @@ -166,11 +166,11 @@ class StoragePluginModule(priority: Int) extends ModuleDef { supervisor: Supervisor, storagesStatistics: StoragesStatistics, xas: Transactors, - clock: Clock[UIO], + clock: Clock[IO], uuidF: UUIDF, as: ActorSystem[Nothing], remoteDiskStorageClient: RemoteDiskStorageClient, - scheduler: Scheduler + cs: ContextShift[IO] ) => IO .delay( @@ -186,7 +186,7 @@ class StoragePluginModule(priority: Int) extends ModuleDef { )( clock, uuidF, - scheduler, + cs, as ) ) @@ -205,7 +205,6 @@ class StoragePluginModule(priority: Int) extends ModuleDef { indexingAction: AggregateIndexingAction, shift: File.Shift, baseUri: BaseUri, - s: Scheduler, cr: RemoteContextResolution @Id("aggregate"), ordering: JsonKeyOrdering, fusionConfig: FusionConfig @@ -214,7 +213,6 @@ class StoragePluginModule(priority: Int) extends ModuleDef { new FilesRoutes(identities, aclCheck, files, schemeDirectives, indexingAction(_, _, _)(shift))( baseUri, storageConfig, - s, cr, ordering, fusionConfig 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 711e76f709..9e08404aac 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 @@ -4,13 +4,13 @@ import akka.actor.typed.ActorSystem import akka.actor.{ActorSystem => ClassicActorSystem} import akka.http.scaladsl.model.ContentTypes.`application/octet-stream` import akka.http.scaladsl.model.{ContentType, HttpEntity, Uri} -import cats.effect.Clock +import cats.effect.{Clock, ContextShift, IO} import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.RetryStrategy import ch.epfl.bluebrain.nexus.delta.kernel.cache.KeyValueStore -import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration.ioToTaskK +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration.{ioToTaskK, toCatsIOOps, toMonixBIOOps} import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent -import ch.epfl.bluebrain.nexus.delta.kernel.utils.{IOUtils, UUIDF} +import ch.epfl.bluebrain.nexus.delta.kernel.utils.{IOInstant, UUIDF} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.Files._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.Digest.{ComputedDigest, NotComputedDigest} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileAttributes.FileAttributesOrigin.Client @@ -22,19 +22,20 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.schemas.{files => fil import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.StorageTypeConfig import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageRejection.StorageIsDeprecated import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{DigestAlgorithm, Storage, StorageType} -import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.StorageFileRejection.FetchFileRejection +import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.StorageFileRejection.{FetchAttributeRejection, FetchFileRejection, SaveFileRejection} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.remote.client.RemoteDiskStorageClient -import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.{FetchAttributes, FetchFile, LinkFile, SaveFile} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.{Storages, StoragesStatistics} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.ContextValue +import ch.epfl.bluebrain.nexus.delta.sdk.AkkaSource import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.directives.FileResponse import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.ExpandIri import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegmentRef.{Latest, Revision, Tag} import ch.epfl.bluebrain.nexus.delta.sdk.model._ +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectContext} import ch.epfl.bluebrain.nexus.delta.sourcing.ScopedEntityDefinition.Tagger @@ -47,8 +48,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.stream.Elem.{DroppedElem, SuccessE import ch.epfl.bluebrain.nexus.delta.sourcing.stream.{CompiledProjection, ExecutionStrategy, ProjectionMetadata, Supervisor} import com.typesafe.scalalogging.Logger import fs2.Stream -import monix.bio.{IO, Task, UIO} -import monix.execution.Scheduler +import monix.bio.{IO => BIO, Task} import java.util.UUID @@ -94,9 +94,9 @@ final class Files( projectRef: ProjectRef, entity: HttpEntity, tag: Option[UserTag] - )(implicit caller: Caller): IO[FileRejection, FileResource] = { + )(implicit caller: Caller): IO[FileResource] = { for { - pc <- fetchContext.onCreate(projectRef) + pc <- fetchContext.onCreate(projectRef).toCatsIO iri <- generateId(pc) _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag)) (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) @@ -120,19 +120,17 @@ final class Files( * the optional tag this file is being created with, attached to the current revision */ def create( - id: IdSegment, + id: FileId, storageId: Option[IdSegment], - projectRef: ProjectRef, entity: HttpEntity, tag: Option[UserTag] - )(implicit caller: Caller): IO[FileRejection, FileResource] = { + )(implicit caller: Caller): IO[FileResource] = { for { - pc <- fetchContext.onCreate(projectRef) - iri <- expandIri(id, pc) - _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) + (iri, pc) <- id.expandIri(fetchContext.onCreate) + _ <- test(CreateFile(iri, id.project, testStorageRef, testStorageType, testAttributes, caller.subject, tag)) + (storageRef, storage) <- fetchActiveStorage(storageId, id.project, pc) attributes <- extractFileAttributes(iri, entity, storage) - res <- eval(CreateFile(iri, projectRef, storageRef, storage.tpe, attributes, caller.subject, tag)) + res <- eval(CreateFile(iri, id.project, storageRef, storage.tpe, attributes, caller.subject, tag)) } yield res }.span("createFile") @@ -159,9 +157,9 @@ final class Files( mediaType: Option[ContentType], path: Uri.Path, tag: Option[UserTag] - )(implicit caller: Caller): IO[FileRejection, FileResource] = { + )(implicit caller: Caller): IO[FileResource] = { for { - pc <- fetchContext.onCreate(projectRef) + pc <- fetchContext.onCreate(projectRef).toCatsIO iri <- generateId(pc) res <- createLink(iri, projectRef, pc, storageId, filename, mediaType, path, tag) } yield res @@ -186,18 +184,16 @@ final class Files( * the optional tag this file link is being created with, attached to the current revision */ def createLink( - id: IdSegment, + id: FileId, storageId: Option[IdSegment], - projectRef: ProjectRef, filename: Option[String], mediaType: Option[ContentType], path: Uri.Path, tag: Option[UserTag] - )(implicit caller: Caller): IO[FileRejection, FileResource] = { + )(implicit caller: Caller): IO[FileResource] = { for { - pc <- fetchContext.onCreate(projectRef) - iri <- expandIri(id, pc) - res <- createLink(iri, projectRef, pc, storageId, filename, mediaType, path, tag) + (iri, pc) <- id.expandIri(fetchContext.onCreate) + res <- createLink(iri, id.project, pc, storageId, filename, mediaType, path, tag) } yield res }.span("createLink") @@ -216,19 +212,17 @@ final class Files( * the http FormData entity */ def update( - id: IdSegment, + id: FileId, storageId: Option[IdSegment], - projectRef: ProjectRef, rev: Int, entity: HttpEntity - )(implicit caller: Caller): IO[FileRejection, FileResource] = { + )(implicit caller: Caller): IO[FileResource] = { for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - _ <- test(UpdateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) + (iri, pc) <- id.expandIri(fetchContext.onModify) + _ <- test(UpdateFile(iri, id.project, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) + (storageRef, storage) <- fetchActiveStorage(storageId, id.project, pc) attributes <- extractFileAttributes(iri, entity, storage) - res <- eval(UpdateFile(iri, projectRef, storageRef, storage.tpe, attributes, rev, caller.subject)) + res <- eval(UpdateFile(iri, id.project, storageRef, storage.tpe, attributes, rev, caller.subject)) } yield res }.span("updateFile") @@ -251,25 +245,21 @@ final class Files( * the path where the file is located inside the storage */ def updateLink( - id: IdSegment, + id: FileId, storageId: Option[IdSegment], - projectRef: ProjectRef, filename: Option[String], mediaType: Option[ContentType], path: Uri.Path, rev: Int - )(implicit caller: Caller): IO[FileRejection, FileResource] = { + )(implicit caller: Caller): IO[FileResource] = { for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - _ <- test(UpdateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) - resolvedFilename <- IO.fromOption(filename.orElse(path.lastSegment), InvalidFileLink(iri)) + (iri, pc) <- id.expandIri(fetchContext.onModify) + _ <- test(UpdateFile(iri, id.project, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) + (storageRef, storage) <- fetchActiveStorage(storageId, id.project, pc) + resolvedFilename <- IO.fromOption(filename.orElse(path.lastSegment))(InvalidFileLink(iri)) description <- FileDescription(resolvedFilename, mediaType) - attributes <- LinkFile(storage, remoteDiskStorageClient, config) - .apply(path, description) - .mapError(LinkRejection(iri, storage.id, _)) - res <- eval(UpdateFile(iri, projectRef, storageRef, storage.tpe, attributes, rev, caller.subject)) + attributes <- linkFile(storage, path, description, iri) + res <- eval(UpdateFile(iri, id.project, storageRef, storage.tpe, attributes, rev, caller.subject)) } yield res }.span("updateLink") @@ -288,16 +278,14 @@ final class Files( * the current revision of the file */ def tag( - id: IdSegment, - projectRef: ProjectRef, + id: FileId, tag: UserTag, tagRev: Int, rev: Int - )(implicit subject: Subject): IO[FileRejection, FileResource] = { + )(implicit subject: Subject): IO[FileResource] = { for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - res <- eval(TagFile(iri, projectRef, tagRev, tag, rev, subject)) + (iri, _) <- id.expandIri(fetchContext.onModify) + res <- eval(TagFile(iri, id.project, tagRev, tag, rev, subject)) } yield res }.span("tagFile") @@ -314,15 +302,13 @@ final class Files( * the current revision of the file */ def deleteTag( - id: IdSegment, - projectRef: ProjectRef, + id: FileId, tag: UserTag, rev: Int - )(implicit subject: Subject): IO[FileRejection, FileResource] = { + )(implicit subject: Subject): IO[FileResource] = { for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - res <- eval(DeleteFileTag(iri, projectRef, tag, rev, subject)) + (iri, _) <- id.expandIri(fetchContext.onModify) + res <- eval(DeleteFileTag(iri, id.project, tag, rev, subject)) } yield res }.span("deleteFileTag") @@ -337,14 +323,12 @@ final class Files( * the current revision of the file */ def deprecate( - id: IdSegment, - projectRef: ProjectRef, + id: FileId, rev: Int - )(implicit subject: Subject): IO[FileRejection, FileResource] = { + )(implicit subject: Subject): IO[FileResource] = { for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - res <- eval(DeprecateFile(iri, projectRef, rev, subject)) + (iri, _) <- id.expandIri(fetchContext.onModify) + res <- eval(DeprecateFile(iri, id.project, rev, subject)) } yield res }.span("deprecateFile") @@ -356,21 +340,22 @@ final class Files( * @param project * the project where the storage belongs */ - def fetchContent(id: IdSegmentRef, project: ProjectRef)(implicit caller: Caller): IO[FileRejection, FileResponse] = { + def fetchContent(id: FileId)(implicit caller: Caller): IO[FileResponse] = { for { - file <- fetch(id, project) + file <- fetch(id) attributes = file.value.attributes - storage <- storages.fetch(file.value.storage, project) - permission = storage.value.storageValue.readPermission - _ <- aclCheck.authorizeForOr(project, permission)(AuthorizationFailed(project, permission)) - s = FetchFile(storage.value, remoteDiskStorageClient, config) - .apply(attributes) - .mapError(FetchRejection(file.id, storage.id, _)) - .leftWiden[FileRejection] + storage <- storages.fetch(file.value.storage, id.project).toCatsIO + _ <- validateAuth(id.project, storage.value.storageValue.readPermission) + s = fetchFile(storage.value, attributes, file.id) mediaType = attributes.mediaType.getOrElse(`application/octet-stream`) } yield FileResponse(attributes.filename, mediaType, attributes.bytes, s) }.span("fetchFileContent") + private def fetchFile(storage: Storage, attr: FileAttributes, fileId: Iri): BIO[FileRejection, AkkaSource] = + FetchFile(storage, remoteDiskStorageClient, config) + .apply(attr) + .mapError(FetchRejection(fileId, storage.id, _)) + /** * Fetch the last version of a file * @@ -379,21 +364,22 @@ final class Files( * @param project * the project where the storage belongs */ - def fetch(id: IdSegmentRef, project: ProjectRef): IO[FileRejection, FileResource] = { + def fetch(id: FileId): IO[FileResource] = { for { - pc <- fetchContext.onRead(project) - iri <- expandIri(id.value, pc) - notFound = FileNotFound(iri, project) - state <- id match { - case Latest(_) => log.stateOr(project, iri, notFound) - case Revision(_, rev) => - log.stateOr(project, iri, rev, notFound, RevisionNotFound) - case Tag(_, tag) => - log.stateOr(project, iri, tag, notFound, TagNotFound(tag)) - } + (iri, _) <- id.expandIri(fetchContext.onRead) + state <- fetchState(id, iri) } yield state.toResource }.span("fetchFile") + private def fetchState(id: FileId, iri: Iri): IO[FileState] = { + val notFound = FileNotFound(iri, id.project) + id.id match { + case Latest(_) => log.stateOr(id.project, iri, notFound) + case Revision(_, rev) => log.stateOr(id.project, iri, rev, notFound, RevisionNotFound) + case Tag(_, tag) => log.stateOr(id.project, iri, tag, notFound, TagNotFound(tag)) + } + }.toCatsIO + private def createLink( iri: Iri, ref: ProjectRef, @@ -403,64 +389,71 @@ final class Files( mediaType: Option[ContentType], path: Uri.Path, tag: Option[UserTag] - )(implicit caller: Caller): IO[FileRejection, FileResource] = + )(implicit caller: Caller): IO[FileResource] = for { _ <- test(CreateFile(iri, ref, testStorageRef, testStorageType, testAttributes, caller.subject, tag)) (storageRef, storage) <- fetchActiveStorage(storageId, ref, pc) - resolvedFilename <- IO.fromOption(filename.orElse(path.lastSegment), InvalidFileLink(iri)) + resolvedFilename <- IO.fromOption(filename.orElse(path.lastSegment))(InvalidFileLink(iri)) description <- FileDescription(resolvedFilename, mediaType) - attributes <- LinkFile(storage, remoteDiskStorageClient, config) - .apply(path, description) - .mapError(LinkRejection(iri, storage.id, _)) + attributes <- linkFile(storage, path, description, iri) res <- eval(CreateFile(iri, ref, storageRef, storage.tpe, attributes, caller.subject, tag)) } yield res - private def eval(cmd: FileCommand): IO[FileRejection, FileResource] = - log.evaluate(cmd.project, cmd.id, cmd).map(_._2.toResource) + private def linkFile(storage: Storage, path: Uri.Path, desc: FileDescription, fileId: Iri): IO[FileAttributes] = + LinkFile(storage, remoteDiskStorageClient, config) + .apply(path, desc) + .toCatsIO + .adaptError { case e: StorageFileRejection => LinkRejection(fileId, storage.id, e) } - private def test(cmd: FileCommand) = log.dryRun(cmd.project, cmd.id, cmd) + private def eval(cmd: FileCommand): IO[FileResource] = + log.evaluate(cmd.project, cmd.id, cmd).map(_._2.toResource).toCatsIO + + private def test(cmd: FileCommand) = log.dryRun(cmd.project, cmd.id, cmd).toCatsIO private def fetchActiveStorage(storageIdOpt: Option[IdSegment], ref: ProjectRef, pc: ProjectContext)(implicit caller: Caller - ): IO[FileRejection, (ResourceRef.Revision, Storage)] = + ): IO[(ResourceRef.Revision, Storage)] = storageIdOpt match { case Some(storageId) => for { - iri <- expandStorageIri(storageId, pc) - storage <- storages.fetch(ResourceRef(iri), ref) - _ <- IO.when(storage.deprecated)(IO.raiseError(WrappedStorageRejection(StorageIsDeprecated(iri)))) - permission = storage.value.storageValue.writePermission - _ <- aclCheck.authorizeForOr(ref, permission)(AuthorizationFailed(ref, permission)) + iri <- expandStorageIri(storageId, pc) + storage <- storages.fetch(ResourceRef(iri), ref).toCatsIO + _ <- IO.whenA(storage.deprecated)(IO.raiseError(WrappedStorageRejection(StorageIsDeprecated(iri)))) + _ <- validateAuth(ref, storage.value.storageValue.writePermission) } yield ResourceRef.Revision(storage.id, storage.rev) -> storage.value case None => for { - storage <- storages.fetchDefault(ref).mapError(WrappedStorageRejection) - permission = storage.value.storageValue.writePermission - _ <- aclCheck.authorizeForOr(ref, permission)(AuthorizationFailed(ref, permission)) + storage <- storages.fetchDefault(ref).mapError(WrappedStorageRejection).toCatsIO + _ <- validateAuth(ref, storage.value.storageValue.writePermission) } yield ResourceRef.Revision(storage.id, storage.rev) -> storage.value } - private def extractFileAttributes(iri: Iri, entity: HttpEntity, storage: Storage): IO[FileRejection, FileAttributes] = + private def validateAuth(project: ProjectRef, permission: Permission)(implicit c: Caller): IO[Unit] = + aclCheck.authorizeForOr(project, permission)(AuthorizationFailed(project, permission)).toCatsIO + + private def extractFileAttributes(iri: Iri, entity: HttpEntity, storage: Storage): IO[FileAttributes] = for { - storageAvailableSpace <- storage.storageValue.capacity.fold(UIO.none[Long]) { capacity => + storageAvailableSpace <- storage.storageValue.capacity.fold(IO.none[Long]) { capacity => storagesStatistics .get(storage.id, storage.project) .redeem( _ => Some(capacity), stat => Some(capacity - stat.spaceUsed) ) + .toCatsIO } (description, source) <- formDataExtractor(iri, entity, storage.storageValue.maxFileSize, storageAvailableSpace) attributes <- SaveFile(storage, remoteDiskStorageClient, config) .apply(description, source) - .mapError(SaveRejection(iri, storage.id, _)) + .toCatsIO + .adaptError { case e: SaveFileRejection => SaveRejection(iri, storage.id, e) } } yield attributes - private def expandStorageIri(segment: IdSegment, pc: ProjectContext): IO[WrappedStorageRejection, Iri] = - Storages.expandIri(segment, pc).mapError(WrappedStorageRejection) + private def expandStorageIri(segment: IdSegment, pc: ProjectContext): IO[Iri] = + Storages.expandIri(segment, pc).mapError(WrappedStorageRejection).toCatsIO - private def generateId(pc: ProjectContext)(implicit uuidF: UUIDF): UIO[Iri] = - uuidF().map(uuid => pc.base.iri / uuid.toString) + private def generateId(pc: ProjectContext)(implicit uuidF: UUIDF): IO[Iri] = + uuidF().toCatsIO.map(uuid => pc.base.iri / uuid.toString) /** * Starts a stream that attempts to update file attributes asynchronously for linked files in remote storages @@ -523,37 +516,37 @@ final class Files( for { _ <- Task.delay(logger.info(s"Updating attributes for file ${f.id} in ${f.project}")) storage <- fetchStorage(f) - _ <- updateAttributes(f, storage) + _ <- updateAttributes(f, storage).toBIO[FileRejection] } yield () }.retry(retryStrategy) } } yield stream } - private[files] def updateAttributes(iri: Iri, project: ProjectRef): IO[FileRejection, Unit] = + private[files] def updateAttributes(iri: Iri, project: ProjectRef): IO[Unit] = for { - f <- log.stateOr(project, iri, FileNotFound(iri, project)) - storage <- storages - .fetch(IdSegmentRef(f.storage), f.project) - .bimap(WrappedStorageRejection, _.value) + f <- log.stateOr(project, iri, FileNotFound(iri, project)).toCatsIO + storage <- storages.fetch(IdSegmentRef(f.storage), f.project).bimap(WrappedStorageRejection, _.value).toCatsIO _ <- updateAttributes(f: FileState, storage: Storage) } yield () - private def updateAttributes(f: FileState, storage: Storage): IO[FileRejection, Unit] = { + private def updateAttributes(f: FileState, storage: Storage): IO[Unit] = { val attr = f.attributes for { _ <- IO.raiseWhen(f.attributes.digest.computed)(DigestAlreadyComputed(f.id)) - newAttr <- - FetchAttributes(storage, remoteDiskStorageClient) - .apply(attr) - .mapError(FetchAttributesRejection(f.id, storage.id, _)) - _ <- IO.raiseWhen(!newAttr.digest.computed)(DigestNotComputed(f.id)) + newAttr <- fetchAttributes(storage, attr, f.id) mediaType = attr.mediaType orElse Some(newAttr.mediaType) command = UpdateFileAttributes(f.id, f.project, mediaType, newAttr.bytes, newAttr.digest, f.rev, f.updatedBy) - _ <- log.evaluate(f.project, f.id, command) + _ <- log.evaluate(f.project, f.id, command).toCatsIO } yield () } + private def fetchAttributes(storage: Storage, attr: FileAttributes, fileId: Iri): IO[ComputedFileAttributes] = + FetchAttributes(storage, remoteDiskStorageClient) + .apply(attr) + .toCatsIO + .adaptError { case e: FetchAttributeRejection => FetchAttributesRejection(fileId, storage.id, e) } + } object Files { @@ -565,8 +558,6 @@ object Files { */ final val entityType: EntityType = EntityType("file") - val expandIri: ExpandIri[InvalidFileId] = new ExpandIri(InvalidFileId.apply) - val context: ContextValue = ContextValue(contexts.files) /** @@ -617,12 +608,12 @@ object Files { } private[files] def evaluate(state: Option[FileState], cmd: FileCommand)(implicit - clock: Clock[UIO] - ): IO[FileRejection, FileEvent] = { + clock: Clock[IO] + ): IO[FileEvent] = { def create(c: CreateFile) = state match { case None => - IOUtils.instant.map( + IOInstant.now.map( FileCreated(c.id, c.project, c.storage, c.storageType, c.attributes, 1, _, c.subject, c.tag) ) case Some(_) => @@ -635,7 +626,7 @@ object Files { case Some(s) if s.deprecated => IO.raiseError(FileIsDeprecated(c.id)) case Some(s) if s.attributes.digest == NotComputedDigest => IO.raiseError(DigestNotComputed(c.id)) case Some(s) => - IOUtils.instant + IOInstant.now .map(FileUpdated(c.id, c.project, c.storage, c.storageType, c.attributes, s.rev + 1, _, c.subject)) } @@ -646,7 +637,7 @@ object Files { case Some(s) if s.attributes.digest.computed => IO.raiseError(DigestAlreadyComputed(s.id)) case Some(s) => // format: off - IOUtils.instant + IOInstant.now .map(FileAttributesUpdated(c.id, c.project, s.storage, s.storageType, c.mediaType, c.bytes, c.digest, s.rev + 1, _, c.subject)) // format: on } @@ -656,7 +647,7 @@ object Files { case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev)) case Some(s) if c.targetRev <= 0L || c.targetRev > s.rev => IO.raiseError(RevisionNotFound(c.targetRev, s.rev)) case Some(s) => - IOUtils.instant.map( + IOInstant.now.map( FileTagAdded(c.id, c.project, s.storage, s.storageType, c.targetRev, c.tag, s.rev + 1, _, c.subject) ) } @@ -667,7 +658,9 @@ object Files { case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev)) case Some(s) if !s.tags.contains(c.tag) => IO.raiseError(TagNotFound(c.tag)) case Some(s) => - IOUtils.instant.map(FileTagDeleted(c.id, c.project, s.storage, s.storageType, c.tag, s.rev + 1, _, c.subject)) + IOInstant.now.map( + FileTagDeleted(c.id, c.project, s.storage, s.storageType, c.tag, s.rev + 1, _, c.subject) + ) } def deprecate(c: DeprecateFile) = state match { @@ -675,7 +668,7 @@ object Files { case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev)) case Some(s) if s.deprecated => IO.raiseError(FileIsDeprecated(c.id)) case Some(s) => - IOUtils.instant.map(FileDeprecated(c.id, c.project, s.storage, s.storageType, s.rev + 1, _, c.subject)) + IOInstant.now.map(FileDeprecated(c.id, c.project, s.storage, s.storageType, s.rev + 1, _, c.subject)) } cmd match { @@ -692,11 +685,11 @@ object Files { * Entity definition for [[Files]] */ def definition(implicit - clock: Clock[UIO] + clock: Clock[IO] ): ScopedEntityDefinition[Iri, FileState, FileCommand, FileEvent, FileRejection] = ScopedEntityDefinition( entityType, - StateMachine(None, evaluate, next), + StateMachine(None, evaluate(_, _).toBIO[FileRejection], next), FileEvent.serializer, FileState.serializer, Tagger[FileEvent]( @@ -731,9 +724,9 @@ object Files { config: FilesConfig, remoteDiskStorageClient: RemoteDiskStorageClient )(implicit - clock: Clock[UIO], + clock: Clock[IO], uuidF: UUIDF, - scheduler: Scheduler, + cs: ContextShift[IO], as: ActorSystem[Nothing] ): Files = { implicit val classicAs: ClassicActorSystem = as.classicSystem 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 e112d234d7..ea0c4e0c5c 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 @@ -8,16 +8,15 @@ import akka.http.scaladsl.server._ import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, MultipartUnmarshallers, Unmarshaller} import akka.stream.scaladsl.{Keep, Sink} +import cats.effect.{ContextShift, IO} import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.{FileUtils, UUIDF} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileDescription import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{FileTooLarge, InvalidMultipartFieldName, WrappedAkkaRejection} -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileDescription, FileRejection} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri -import monix.bio.IO -import monix.execution.Scheduler -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.util.Try sealed trait FormDataExtractor { @@ -41,7 +40,7 @@ sealed trait FormDataExtractor { entity: HttpEntity, maxFileSize: Long, storageAvailableSpace: Option[Long] - ): IO[FileRejection, (FileDescription, BodyPartEntity)] + ): IO[(FileDescription, BodyPartEntity)] } object FormDataExtractor { @@ -65,26 +64,28 @@ object FormDataExtractor { def apply( mediaTypeDetector: MediaTypeDetectorConfig - )(implicit uuidF: UUIDF, as: ActorSystem, sc: Scheduler): FormDataExtractor = + )(implicit uuidF: UUIDF, as: ActorSystem, cs: ContextShift[IO]): FormDataExtractor = new FormDataExtractor { + implicit val ec: ExecutionContext = as.getDispatcher + override def apply( id: Iri, entity: HttpEntity, maxFileSize: Long, storageAvailableSpace: Option[Long] - ): IO[FileRejection, (FileDescription, BodyPartEntity)] = { + ): IO[(FileDescription, BodyPartEntity)] = { val sizeLimit = Math.min(storageAvailableSpace.getOrElse(Long.MaxValue), maxFileSize) for { formData <- unmarshall(entity, sizeLimit) fileOpt <- extractFile(formData, maxFileSize, storageAvailableSpace) - file <- IO.fromOption(fileOpt, InvalidMultipartFieldName(id)) + file <- IO.fromOption(fileOpt)(InvalidMultipartFieldName(id)) } yield file } - private def unmarshall(entity: HttpEntity, sizeLimit: Long) = - IO.deferFuture(um(entity.withSizeLimit(sizeLimit))).mapError(onUnmarshallingError) + private def unmarshall(entity: HttpEntity, sizeLimit: Long): IO[FormData] = + IO.fromFuture(IO.delay(um(entity.withSizeLimit(sizeLimit)))).adaptError(onUnmarshallingError(_)) - private def onUnmarshallingError(th: Throwable) = th match { + private def onUnmarshallingError(th: Throwable): WrappedAkkaRejection = th match { case RejectionError(r) => WrappedAkkaRejection(r) case Unmarshaller.NoContentException => @@ -103,15 +104,17 @@ object FormDataExtractor { formData: FormData, maxFileSize: Long, storageAvailableSpace: Option[Long] - ): IO[FileRejection, Option[(FileDescription, BodyPartEntity)]] = IO + ): IO[Option[(FileDescription, BodyPartEntity)]] = IO .fromFuture( - formData.parts - .mapAsync(parallelism = 1)(extractFile) - .collect { case Some(values) => values } - .toMat(Sink.headOption)(Keep.right) - .run() + IO( + formData.parts + .mapAsync(parallelism = 1)(extractFile) + .collect { case Some(values) => values } + .toMat(Sink.headOption)(Keep.right) + .run() + ) ) - .mapError { + .adaptError { case _: EntityStreamSizeException => FileTooLarge(maxFileSize, storageAvailableSpace) case th => @@ -122,7 +125,7 @@ object FormDataExtractor { case part if part.name == fieldName => val filename = part.filename.getOrElse("file") val contentType = detectContentType(filename, part.entity.contentType) - FileDescription(filename, contentType).runToFuture.map { desc => + FileDescription(filename, contentType).unsafeToFuture().map { desc => Some(desc -> part.entity) } case part => diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/File.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/File.scala index 758baec542..e33bda375c 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/File.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/File.scala @@ -11,12 +11,10 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.sdk.ResourceShift import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegmentRef, Tags} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef -import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, Tags} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import io.circe.syntax._ -import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import io.circe.{Encoder, Json} /** @@ -75,7 +73,7 @@ object File { def shift(files: Files)(implicit baseUri: BaseUri, config: StorageTypeConfig): Shift = ResourceShift.withMetadata[FileState, File, Metadata]( Files.entityType, - (ref, project) => files.fetch(IdSegmentRef(ref), project).toCatsIO, + (ref, project) => files.fetch(FileId(ref, project)), state => state.toResource, value => JsonLdContent(value, value.value.asJson, Some(value.value.metadata)) ) 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 523c5a9fd9..9da8386f53 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 @@ -1,8 +1,9 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model import akka.http.scaladsl.model.ContentType +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration.toCatsIOOps import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF -import monix.bio.UIO import java.util.UUID @@ -20,9 +21,9 @@ final case class FileDescription(uuid: UUID, filename: String, mediaType: Option object FileDescription { - final def apply(filename: String, mediaType: Option[ContentType])(implicit uuidF: UUIDF): UIO[FileDescription] = - uuidF().map(FileDescription(_, filename, mediaType)) + final def apply(filename: String, mediaType: Option[ContentType])(implicit uuidF: UUIDF): IO[FileDescription] = + uuidF().toCatsIO.map(FileDescription(_, filename, mediaType)) - final def apply(filename: String, mediaType: ContentType)(implicit uuidF: UUIDF): UIO[FileDescription] = + final def apply(filename: String, mediaType: ContentType)(implicit uuidF: UUIDF): IO[FileDescription] = apply(filename, Some(mediaType)) } diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileId.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileId.scala new file mode 100644 index 0000000000..b6d4a6b855 --- /dev/null +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileId.scala @@ -0,0 +1,26 @@ +package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model + +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration.toCatsIOOps +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileId.iriExpander +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.InvalidFileId +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri +import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.ExpandIri +import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegment, IdSegmentRef} +import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectContext +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} +import monix.bio.{IO => BIO} + +final case class FileId(id: IdSegmentRef, project: ProjectRef) { + def expandIri(fetchContext: ProjectRef => BIO[FileRejection, ProjectContext]): IO[(Iri, ProjectContext)] = + fetchContext(project).flatMap(pc => iriExpander(id.value, pc).map(iri => (iri, pc))).toCatsIO +} + +object FileId { + def apply(ref: ResourceRef, project: ProjectRef): FileId = FileId(IdSegmentRef(ref), project) + def apply(id: IdSegment, tag: UserTag, project: ProjectRef): FileId = FileId(IdSegmentRef(id, tag), project) + def apply(id: IdSegment, rev: Int, project: ProjectRef): FileId = FileId(IdSegmentRef(id, rev), project) + + val iriExpander: ExpandIri[InvalidFileId] = new ExpandIri(InvalidFileId.apply) +} 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 02b28446fe..2e805700cb 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 @@ -1,7 +1,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Rejection +import akka.http.scaladsl.server.{Rejection => AkkaRejection} import ch.epfl.bluebrain.nexus.delta.kernel.Mapper import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClassUtils import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageRejection @@ -22,6 +22,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.syntax.httpResponseFieldsSyntax import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import ch.epfl.bluebrain.nexus.delta.sourcing.rejection.Rejection import com.typesafe.scalalogging.Logger import io.circe.syntax._ import io.circe.{Encoder, JsonObject} @@ -32,9 +33,7 @@ import io.circe.{Encoder, JsonObject} * @param reason * a descriptive message as to why the rejection occurred */ -sealed abstract class FileRejection(val reason: String, val loggedDetails: Option[String] = None) extends Exception { - override def getMessage: String = reason -} +sealed abstract class FileRejection(val reason: String, val loggedDetails: Option[String] = None) extends Rejection object FileRejection { @@ -178,7 +177,7 @@ object FileRejection { /** * Rejection returned when attempting to create/update a file and the unmarshaller fails */ - final case class WrappedAkkaRejection(rejection: Rejection) extends FileRejection(rejection.toString) + final case class WrappedAkkaRejection(rejection: AkkaRejection) extends FileRejection(rejection.toString) /** * Rejection returned when interacting with the storage operations bundle to fetch a storage diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala index d806843510..fb0845ea24 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala @@ -6,9 +6,11 @@ import akka.http.scaladsl.model.headers.Accept import akka.http.scaladsl.model.{ContentType, MediaRange} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ +import cats.effect.IO +import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{File, FileRejection} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._ +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{File, FileId, FileRejection} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.permissions.{read => Read, write => Write} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FilesRoutes._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{schemas, FileResource, Files} @@ -17,23 +19,21 @@ 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._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck +import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling -import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives.{baseUriPrefix, idSegment, indexingMode, noParameter, tagParam} import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaSchemeDirectives} import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig 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.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment, IdSegmentRef, ResourceF} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +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 kamon.instrumentation.akka.http.TracingDirectives.operationName -import monix.bio.IO -import monix.execution.Scheduler import scala.annotation.nowarn @@ -60,24 +60,24 @@ final class FilesRoutes( )(implicit baseUri: BaseUri, storageConfig: StorageTypeConfig, - s: Scheduler, cr: RemoteContextResolution, ordering: JsonKeyOrdering, fusionConfig: FusionConfig ) extends AuthDirectives(identities, aclCheck) - with CirceUnmarshalling { + with CirceUnmarshalling { self => import baseUri.prefixSegment import schemeDirectives._ - private def indexUIO(project: ProjectRef, resource: ResourceF[File], mode: IndexingMode) = - index(project, resource, mode).toUIO - def routes: Route = (baseUriPrefix(baseUri.prefix) & replaceUri("files", schemas.files)) { pathPrefix("files") { extractCaller { implicit caller => resolveProjectRef.apply { ref => + implicit class IndexOps(io: IO[FileResource]) { + def index(m: IndexingMode): IO[FileResource] = io.flatTap(self.index(ref, _, m)) + } + concat( (post & pathEndOrSingleSlash & noParameter("rev") & parameter( "storage".as[IdSegment].? @@ -88,17 +88,24 @@ final class FilesRoutes( entity(as[LinkFile]) { case LinkFile(filename, mediaType, path) => emit( Created, - files.createLink(storage, ref, filename, mediaType, path, tag).tapEval(indexUIO(ref, _, mode)) + files + .createLink(storage, ref, filename, mediaType, path, tag) + .index(mode) + .attemptNarrow[FileRejection] ) }, // Create a file without id segment extractRequestEntity { entity => - emit(Created, files.create(storage, ref, entity, tag).tapEval(indexUIO(ref, _, mode))) + emit( + Created, + files.create(storage, ref, entity, tag).index(mode).attemptNarrow[FileRejection] + ) } ) } }, (idSegment & indexingMode) { (id, mode) => + val fileId = FileId(id, ref) concat( pathEndOrSingleSlash { operationName(s"$prefixSegment/files/{org}/{project}/{id}") { @@ -112,15 +119,16 @@ final class FilesRoutes( emit( Created, files - .createLink(id, storage, ref, filename, mediaType, path, tag) - .tapEval(indexUIO(ref, _, mode)) + .createLink(fileId, storage, filename, mediaType, path, tag) + .index(mode) + .attemptNarrow[FileRejection] ) }, // Create a file with id segment extractRequestEntity { entity => emit( Created, - files.create(id, storage, ref, entity, tag).tapEval(indexUIO(ref, _, mode)) + files.create(fileId, storage, entity, tag).index(mode).attemptNarrow[FileRejection] ) } ) @@ -130,13 +138,16 @@ final class FilesRoutes( entity(as[LinkFile]) { case LinkFile(filename, mediaType, path) => emit( files - .updateLink(id, storage, ref, filename, mediaType, path, rev) - .tapEval(indexUIO(ref, _, mode)) + .updateLink(fileId, storage, filename, mediaType, path, rev) + .index(mode) + .attemptNarrow[FileRejection] ) }, // Update a file extractRequestEntity { entity => - emit(files.update(id, storage, ref, rev, entity).tapEval(indexUIO(ref, _, mode))) + emit( + files.update(fileId, storage, rev, entity).index(mode).attemptNarrow[FileRejection] + ) } ) } @@ -144,16 +155,18 @@ final class FilesRoutes( // Deprecate a file (delete & parameter("rev".as[Int])) { rev => authorizeFor(ref, Write).apply { - emit(files.deprecate(id, ref, rev).tapEval(indexUIO(ref, _, mode)).rejectOn[FileNotFound]) + emit( + files + .deprecate(fileId, rev) + .index(mode) + .attemptNarrow[FileRejection] + .rejectOn[FileNotFound] + ) } }, // Fetch a file (get & idSegmentRef(id)) { id => - emitOrFusionRedirect( - ref, - id, - fetch(id, ref) - ) + emitOrFusionRedirect(ref, id, fetch(FileId(id, ref))) } ) } @@ -163,13 +176,21 @@ final class FilesRoutes( concat( // Fetch a file tags (get & idSegmentRef(id) & pathEndOrSingleSlash & authorizeFor(ref, Read)) { id => - emit(fetchMetadata(id, ref).map(_.value.tags).rejectOn[FileNotFound]) + emit( + fetchMetadata(FileId(id, ref)) + .map(_.value.tags) + .attemptNarrow[FileRejection] + .rejectOn[FileNotFound] + ) }, // Tag a file (post & parameter("rev".as[Int]) & pathEndOrSingleSlash) { rev => authorizeFor(ref, Write).apply { entity(as[Tag]) { case Tag(tagRev, tag) => - emit(Created, files.tag(id, ref, tag, tagRev, rev).tapEval(indexUIO(ref, _, mode))) + emit( + Created, + files.tag(fileId, tag, tagRev, rev).index(mode).attemptNarrow[FileRejection] + ) } } }, @@ -179,7 +200,11 @@ final class FilesRoutes( Write )) { (tag, rev) => emit( - files.deleteTag(id, ref, tag, rev).tapEval(indexUIO(ref, _, mode)).rejectOn[FileNotFound] + files + .deleteTag(fileId, tag, rev) + .index(mode) + .attemptNarrow[FileRejection] + .rejectOn[FileNotFound] ) } ) @@ -193,16 +218,16 @@ final class FilesRoutes( } } - def fetch(id: IdSegmentRef, ref: ProjectRef)(implicit caller: Caller): Route = + def fetch(id: FileId)(implicit caller: Caller): Route = (headerValueByType(Accept) & varyAcceptHeaders) { case accept if accept.mediaRanges.exists(metadataMediaRanges.contains) => - emit(fetchMetadata(id, ref).rejectOn[FileNotFound]) + emit(fetchMetadata(id).attemptNarrow[FileRejection].rejectOn[FileNotFound]) case _ => - emit(files.fetchContent(id, ref).rejectOn[FileNotFound]) + emit(files.fetchContent(id).attemptNarrow[FileRejection].rejectOn[FileNotFound]) } - def fetchMetadata(id: IdSegmentRef, ref: ProjectRef)(implicit caller: Caller): IO[FileRejection, FileResource] = - aclCheck.authorizeForOr(ref, Read)(AuthorizationFailed(ref, Read)) >> files.fetch(id, ref) + def fetchMetadata(id: FileId)(implicit caller: Caller): IO[FileResource] = + aclCheck.authorizeForOr(id.project, Read)(AuthorizationFailed(id.project, Read)).toCatsIO >> files.fetch(id) } object FilesRoutes { @@ -224,7 +249,6 @@ object FilesRoutes { index: IndexingAction.Execute[File] )(implicit baseUri: BaseUri, - s: Scheduler, cr: RemoteContextResolution, ordering: JsonKeyOrdering, fusionConfig: FusionConfig diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/StorageFileRejection.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/StorageFileRejection.scala index fe24d1dd33..51fdd985cd 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/StorageFileRejection.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/StorageFileRejection.scala @@ -1,11 +1,14 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageType +import ch.epfl.bluebrain.nexus.delta.sourcing.rejection.Rejection /** * Enumeration of Storage rejections related to file operations. */ -sealed abstract class StorageFileRejection(val loggedDetails: String) extends Product with Serializable +sealed abstract class StorageFileRejection(val loggedDetails: String) extends Rejection { + override def reason: String = loggedDetails +} object StorageFileRejection { diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteDiskStorageClient.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteDiskStorageClient.scala index adf9f2bc37..ea71155cb7 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteDiskStorageClient.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteDiskStorageClient.scala @@ -59,7 +59,7 @@ final class RemoteDiskStorageClient(client: HttpClient, getAuthToken: AuthTokenP * the storage bucket name */ def exists(bucket: Label)(implicit baseUri: BaseUri): IO[HttpClientError, Unit] = { - getAuthToken(credentials).toBIO.flatMap { authToken => + getAuthToken(credentials).toBIOThrowable.flatMap { authToken => val endpoint = baseUri.endpoint / "buckets" / bucket.value val req = Head(endpoint).withCredentials(authToken) client(req) { @@ -83,7 +83,7 @@ final class RemoteDiskStorageClient(client: HttpClient, getAuthToken: AuthTokenP relativePath: Path, entity: BodyPartEntity )(implicit baseUri: BaseUri): IO[SaveFileRejection, RemoteDiskStorageFileAttributes] = { - getAuthToken(credentials).toBIO.flatMap { authToken => + getAuthToken(credentials).toBIOThrowable.flatMap { authToken => val endpoint = baseUri.endpoint / "buckets" / bucket.value / "files" / relativePath val filename = relativePath.lastSegment.getOrElse("filename") val multipartForm = FormData(BodyPart("file", entity, Map("filename" -> filename))).toEntity() @@ -107,7 +107,7 @@ final class RemoteDiskStorageClient(client: HttpClient, getAuthToken: AuthTokenP * the relative path to the file location */ def getFile(bucket: Label, relativePath: Path)(implicit baseUri: BaseUri): IO[FetchFileRejection, AkkaSource] = { - getAuthToken(credentials).toBIO.flatMap { authToken => + getAuthToken(credentials).toBIOThrowable.flatMap { authToken => val endpoint = baseUri.endpoint / "buckets" / bucket.value / "files" / relativePath client.toDataBytes(Get(endpoint).withCredentials(authToken)).mapError { case error @ HttpClientStatusError(_, `NotFound`, _) if !bucketNotFoundType(error) => @@ -130,7 +130,7 @@ final class RemoteDiskStorageClient(client: HttpClient, getAuthToken: AuthTokenP bucket: Label, relativePath: Path )(implicit baseUri: BaseUri): IO[FetchFileRejection, RemoteDiskStorageFileAttributes] = { - getAuthToken(credentials).toBIO.flatMap { authToken => + getAuthToken(credentials).toBIOThrowable.flatMap { authToken => val endpoint = baseUri.endpoint / "buckets" / bucket.value / "attributes" / relativePath client.fromJsonTo[RemoteDiskStorageFileAttributes](Get(endpoint).withCredentials(authToken)).mapError { case error @ HttpClientStatusError(_, `NotFound`, _) if !bucketNotFoundType(error) => @@ -157,7 +157,7 @@ final class RemoteDiskStorageClient(client: HttpClient, getAuthToken: AuthTokenP sourceRelativePath: Path, destRelativePath: Path )(implicit baseUri: BaseUri): IO[MoveFileRejection, RemoteDiskStorageFileAttributes] = { - getAuthToken(credentials).toBIO.flatMap { authToken => + getAuthToken(credentials).toBIOThrowable.flatMap { authToken => val endpoint = baseUri.endpoint / "buckets" / bucket.value / "files" / destRelativePath val payload = Json.obj("source" -> sourceRelativePath.toString.asJson) client.fromJsonTo[RemoteDiskStorageFileAttributes](Put(endpoint, payload).withCredentials(authToken)).mapError { diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala index b48ff79689..b0bd6117b7 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala @@ -12,8 +12,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{AbsolutePat 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 -import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ResourceRef} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label import ch.epfl.bluebrain.nexus.testkit.scalatest.EitherValues import ch.epfl.bluebrain.nexus.testkit.scalatest.bio.BIOValues import monix.bio.Task @@ -36,8 +35,6 @@ trait FileFixtures extends EitherValues with BIOValues { val deprecatedProject = ProjectGen.project("org", "proj-deprecated") val projectWithDeprecatedOrg = ProjectGen.project("org-deprecated", "other-proj") val projectRef = project.ref - val diskId = nxv + "disk" - val diskRev = ResourceRef.Revision(iri"$diskId?rev=1", diskId, 1) val diskId2 = nxv + "disk2" val file1 = nxv + "file1" val file2 = nxv + "file2" diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala index fb6e5f3608..84d17dc734 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala @@ -10,7 +10,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.RemoteContextResolutionFixt import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.Digest.NotComputedDigest import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileAttributes.FileAttributesOrigin.Storage import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._ -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileRejection} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileId, FileRejection} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageRejection.StorageNotFound import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageType.{RemoteDiskStorage => RemoteStorageType} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{StorageRejection, StorageStatEntry, StorageType} @@ -36,24 +36,18 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authent import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture -import ch.epfl.bluebrain.nexus.testkit.CirceLiteral import ch.epfl.bluebrain.nexus.testkit.remotestorage.RemoteStorageDocker +import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec import monix.bio.IO import monix.execution.Scheduler +import org.scalatest.DoNotDiscover import org.scalatest.concurrent.Eventually -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike -import org.scalatest.{DoNotDiscover, Inspectors, OptionValues} @DoNotDiscover class FilesSpec(docker: RemoteStorageDocker) extends TestKit(ActorSystem("FilesSpec")) - with AnyWordSpecLike + with CatsEffectSpec with DoobieScalaTestFixture - with OptionValues - with Matchers - with Inspectors - with CirceLiteral with ConfigFixtures with StorageFixtures with AkkaSourceHelpers @@ -84,8 +78,16 @@ class FilesSpec(docker: RemoteStorageDocker) otherWrite ) - val remoteId = nxv + "remote" - val remoteRev = ResourceRef.Revision(iri"$remoteId?rev=1", remoteId, 1) + val remoteIdIri = nxv + "remote" + val remoteId: IdSegment = remoteIdIri + val remoteRev = ResourceRef.Revision(iri"$remoteIdIri?rev=1", remoteIdIri, 1) + + val diskIdIri = nxv + "disk" + val diskId: IdSegment = nxv + "disk" + val diskRev = ResourceRef.Revision(iri"$diskId?rev=1", diskIdIri, 1) + + val storageIri = nxv + "other-storage" + val storage: IdSegment = nxv + "other-storage" val fetchContext = FetchContextDummy( Map(project.ref -> project.context), @@ -127,6 +129,9 @@ class FilesSpec(docker: RemoteStorageDocker) remoteDiskStorageClient ) + def fileId(file: String): FileId = FileId(file, projectRef) + def fileIdIri(iri: Iri): FileId = FileId(iri, projectRef) + def mkResource( id: Iri, project: ProjectRef, @@ -152,18 +157,18 @@ class FilesSpec(docker: RemoteStorageDocker) "succeed with the id passed" in { val expected = mkResource(file1, projectRef, diskRev, attributes("myfile.txt")) - val actual = files.create("file1", Some(diskId), projectRef, entity("myfile.txt"), None).accepted + val actual = files.create(fileId("file1"), Some(diskId), entity("myfile.txt"), None).accepted actual shouldEqual expected } "succeed and tag with the id passed" in { withUUIDF(uuid2) { val file = files - .create("fileTagged", Some(diskId), projectRef, entity("fileTagged.txt"), Some(tag)) + .create(fileId("fileTagged"), Some(diskId), entity("fileTagged.txt"), Some(tag)) .accepted val attr = attributes("fileTagged.txt", id = uuid2) val expectedData = mkResource(fileTagged, projectRef, diskRev, attr, tags = Tags(tag -> 1)) - val fileByTag = files.fetch(IdSegmentRef("fileTagged", tag), projectRef).accepted + val fileByTag = files.fetch(FileId("fileTagged", tag, projectRef)).accepted file shouldEqual expectedData fileByTag.value.tags.tags should contain(tag) @@ -173,7 +178,7 @@ class FilesSpec(docker: RemoteStorageDocker) "succeed with randomly generated id" in { val expected = mkResource(generatedId, projectRef, diskRev, attributes("myfile2.txt")) val actual = files.create(None, projectRef, entity("myfile2.txt"), None).accepted - val fetched = files.fetch(actual.id, projectRef).accepted + val fetched = files.fetch(FileId(actual.id, projectRef)).accepted actual shouldEqual expected fetched shouldEqual expected @@ -186,7 +191,7 @@ class FilesSpec(docker: RemoteStorageDocker) val file = files .create(None, projectRef, entity("fileTagged2.txt"), Some(tag)) .accepted - val fileByTag = files.fetch(IdSegmentRef(generatedId2, tag), projectRef).accepted + val fileByTag = files.fetch(FileId(generatedId2, tag, projectRef)).accepted file shouldEqual expected fileByTag.value.tags.tags should contain(tag) @@ -195,12 +200,12 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if no write permissions" in { files - .create("file2", Some(remoteId), projectRef, entity(), None) + .create(fileId("file2"), Some(remoteId), entity(), None) .rejectedWith[AuthorizationFailed] } "reject if file id already exists" in { - files.create("file1", None, projectRef, entity(), None).rejected shouldEqual + files.create(fileId("file1"), None, entity(), None).rejected shouldEqual ResourceAlreadyExists(file1, projectRef) } @@ -208,20 +213,19 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if the file exceeds max file size for the storage" in { files - .create("file-too-long", Some(remoteId), projectRef, randomEntity("large_file", 280), None)(aliceCaller) + .create(fileId("file-too-long"), Some(remoteId), randomEntity("large_file", 280), None)(aliceCaller) .rejected shouldEqual FileTooLarge(300L, None) } "reject if the file exceeds the remaining available space on the storage" in { files - .create("file-too-long", Some(diskId), projectRef, randomEntity("large_file", 250), None) + .create(fileId("file-too-long"), Some(diskId), randomEntity("large_file", 250), None) .rejected shouldEqual FileTooLarge(300L, Some(220)) } "reject if storage does not exist" in { - val storage = nxv + "other-storage" - files.create("file2", Some(storage), projectRef, entity(), None).rejected shouldEqual - WrappedStorageRejection(StorageNotFound(storage, projectRef)) + files.create(fileId("file2"), Some(storage), entity(), None).rejected shouldEqual + WrappedStorageRejection(StorageNotFound(storageIri, projectRef)) } "reject if project does not exist" in { @@ -238,7 +242,7 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if no write permissions" in { files - .createLink("file2", Some(remoteId), projectRef, None, None, Uri.Path.Empty, None) + .createLink(fileId("file2"), Some(remoteId), None, None, Uri.Path.Empty, None) .rejectedWith[AuthorizationFailed] } @@ -251,9 +255,9 @@ class FilesSpec(docker: RemoteStorageDocker) val expected = mkResource(file2, projectRef, remoteRev, attr, RemoteStorageType, tags = Tags(tag -> 1)) val result = files - .createLink("file2", Some(remoteId), projectRef, Some("myfile.txt"), None, path, Some(tag)) + .createLink(fileId("file2"), Some(remoteId), Some("myfile.txt"), None, path, Some(tag)) .accepted - val fileByTag = files.fetch(IdSegmentRef("file2", tag), projectRef).accepted + val fileByTag = files.fetch(FileId("file2", tag, projectRef)).accepted result shouldEqual expected fileByTag.value.tags.tags should contain(tag) @@ -261,23 +265,22 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if no filename" in { files - .createLink("file3", Some(remoteId), projectRef, None, None, Uri.Path("a/b/"), None) + .createLink(fileId("file3"), Some(remoteId), None, None, Uri.Path("a/b/"), None) .rejectedWith[InvalidFileLink] } "reject if file id already exists" in { files - .createLink("file2", Some(remoteId), projectRef, None, None, Uri.Path.Empty, None) + .createLink(fileId("file2"), Some(remoteId), None, None, Uri.Path.Empty, None) .rejected shouldEqual ResourceAlreadyExists(file2, projectRef) } "reject if storage does not exist" in { - val storage = nxv + "other-storage" files - .createLink("file3", Some(storage), projectRef, None, None, Uri.Path.Empty, None) + .createLink(fileId("file3"), Some(storage), None, None, Uri.Path.Empty, None) .rejected shouldEqual - WrappedStorageRejection(StorageNotFound(storage, projectRef)) + WrappedStorageRejection(StorageNotFound(storageIri, projectRef)) } "reject if project does not exist" in { @@ -295,28 +298,27 @@ class FilesSpec(docker: RemoteStorageDocker) "updating a file" should { "succeed" in { - files.update("file1", None, projectRef, 1, entity()).accepted shouldEqual + files.update(fileId("file1"), None, 1, entity()).accepted shouldEqual FileGen.resourceFor(file1, projectRef, diskRev, attributes(), rev = 2, createdBy = bob, updatedBy = bob) } "reject if file doesn't exists" in { - files.update(nxv + "other", None, projectRef, 1, entity()).rejectedWith[FileNotFound] + files.update(fileIdIri(nxv + "other"), None, 1, entity()).rejectedWith[FileNotFound] } "reject if storage does not exist" in { - val storage = nxv + "other-storage" - files.update("file1", Some(storage), projectRef, 2, entity()).rejected shouldEqual - WrappedStorageRejection(StorageNotFound(storage, projectRef)) + files.update(fileId("file1"), Some(storage), 2, entity()).rejected shouldEqual + WrappedStorageRejection(StorageNotFound(storageIri, projectRef)) } "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - files.update(file1, None, projectRef, 2, entity()).rejectedWith[ProjectContextRejection] + files.update(FileId(file1, projectRef), None, 2, entity()).rejectedWith[ProjectContextRejection] } "reject if project is deprecated" in { - files.update(file1, None, deprecatedProject.ref, 2, entity()).rejectedWith[ProjectContextRejection] + files.update(FileId(file1, deprecatedProject.ref), None, 2, entity()).rejectedWith[ProjectContextRejection] } } @@ -332,7 +334,7 @@ class FilesSpec(docker: RemoteStorageDocker) val expected = mkResource(file2, projectRef, remoteRev, attr, RemoteStorageType, rev = 2, tags = Tags(tag -> 1)) val updatedF2 = for { _ <- files.updateAttributes(file2, projectRef) - f <- files.fetch(file2, projectRef) + f <- files.fetch(fileIdIri(file2)) } yield f updatedF2.accepted shouldEqual expected } @@ -346,26 +348,26 @@ class FilesSpec(docker: RemoteStorageDocker) val attr = tempAttr.copy(location = s"file:///app/nexustest/nexus/${tempAttr.path}", origin = Storage) val expected = mkResource(file2, projectRef, remoteRev, attr, RemoteStorageType, rev = 3, tags = Tags(tag -> 1)) files - .updateLink("file2", Some(remoteId), projectRef, None, Some(`text/plain(UTF-8)`), path, 2) + .updateLink(fileId("file2"), Some(remoteId), None, Some(`text/plain(UTF-8)`), path, 2) .accepted shouldEqual expected } "reject if file doesn't exists" in { files - .updateLink(nxv + "other", None, projectRef, None, None, Uri.Path.Empty, 1) + .updateLink(fileIdIri(nxv + "other"), None, None, None, Uri.Path.Empty, 1) .rejectedWith[FileNotFound] } "reject if digest is not computed" in { files - .updateLink("file2", None, projectRef, None, None, Uri.Path.Empty, 3) + .updateLink(fileId("file2"), None, None, None, Uri.Path.Empty, 3) .rejectedWith[DigestNotComputed] } "reject if storage does not exist" in { val storage = nxv + "other-storage" files - .updateLink("file1", Some(storage), projectRef, None, None, Uri.Path.Empty, 2) + .updateLink(fileId("file1"), Some(storage), None, None, Uri.Path.Empty, 2) .rejected shouldEqual WrappedStorageRejection(StorageNotFound(storage, projectRef)) } @@ -373,12 +375,14 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - files.updateLink(file1, None, projectRef, None, None, Uri.Path.Empty, 2).rejectedWith[ProjectContextRejection] + files + .updateLink(FileId(file1, projectRef), None, None, None, Uri.Path.Empty, 2) + .rejectedWith[ProjectContextRejection] } "reject if project is deprecated" in { files - .updateLink(file1, None, deprecatedProject.ref, None, None, Uri.Path.Empty, 2) + .updateLink(FileId(file1, deprecatedProject.ref), None, None, None, Uri.Path.Empty, 2) .rejectedWith[ProjectContextRejection] } } @@ -387,39 +391,39 @@ class FilesSpec(docker: RemoteStorageDocker) "succeed" in { val expected = mkResource(file1, projectRef, diskRev, attributes(), rev = 3, tags = Tags(tag -> 1)) - val actual = files.tag(file1, projectRef, tag, tagRev = 1, 2).accepted + val actual = files.tag(fileIdIri(file1), tag, tagRev = 1, 2).accepted actual shouldEqual expected } "reject if file doesn't exists" in { - files.tag(nxv + "other", projectRef, tag, tagRev = 1, 3).rejectedWith[FileNotFound] + files.tag(fileIdIri(nxv + "other"), tag, tagRev = 1, 3).rejectedWith[FileNotFound] } "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - files.tag(rdId, projectRef, tag, tagRev = 2, 4).rejectedWith[ProjectContextRejection] + files.tag(FileId(rdId, projectRef), tag, tagRev = 2, 4).rejectedWith[ProjectContextRejection] } "reject if project is deprecated" in { - files.tag(rdId, deprecatedProject.ref, tag, tagRev = 2, 4).rejectedWith[ProjectContextRejection] + files.tag(FileId(rdId, deprecatedProject.ref), tag, tagRev = 2, 4).rejectedWith[ProjectContextRejection] } } "deleting a tag" should { "succeed" in { val expected = mkResource(file1, projectRef, diskRev, attributes(), rev = 4) - val actual = files.deleteTag(file1, projectRef, tag, 3).accepted + val actual = files.deleteTag(fileIdIri(file1), tag, 3).accepted actual shouldEqual expected } "reject if the file doesn't exist" in { - files.deleteTag(nxv + "other", projectRef, tag, 1).rejectedWith[FileNotFound] + files.deleteTag(fileIdIri(nxv + "other"), tag, 1).rejectedWith[FileNotFound] } "reject if the revision passed is incorrect" in { - files.deleteTag(file1, projectRef, tag, 3).rejected shouldEqual IncorrectRev(expected = 4, provided = 3) + files.deleteTag(fileIdIri(file1), tag, 3).rejected shouldEqual IncorrectRev(expected = 4, provided = 3) } "reject if the tag doesn't exist" in { - files.deleteTag(file1, projectRef, UserTag.unsafe("unknown"), 5).rejected + files.deleteTag(fileIdIri(file1), UserTag.unsafe("unknown"), 5).rejected } } @@ -427,33 +431,33 @@ class FilesSpec(docker: RemoteStorageDocker) "succeed" in { val expected = mkResource(file1, projectRef, diskRev, attributes(), rev = 5, deprecated = true) - val actual = files.deprecate(file1, projectRef, 4).accepted + val actual = files.deprecate(fileIdIri(file1), 4).accepted actual shouldEqual expected } "reject if file doesn't exists" in { - files.deprecate(nxv + "other", projectRef, 1).rejectedWith[FileNotFound] + files.deprecate(fileIdIri(nxv + "other"), 1).rejectedWith[FileNotFound] } "reject if the revision passed is incorrect" in { - files.deprecate(file1, projectRef, 3).rejected shouldEqual + files.deprecate(fileIdIri(file1), 3).rejected shouldEqual IncorrectRev(provided = 3, expected = 5) } "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - files.deprecate(file1, projectRef, 1).rejectedWith[ProjectContextRejection] + files.deprecate(FileId(file1, projectRef), 1).rejectedWith[ProjectContextRejection] } "reject if project is deprecated" in { - files.deprecate(file1, deprecatedProject.ref, 1).rejectedWith[ProjectContextRejection] + files.deprecate(FileId(file1, deprecatedProject.ref), 1).rejectedWith[ProjectContextRejection] } "allow tagging after deprecation" in { val expected = mkResource(file1, projectRef, diskRev, attributes(), rev = 6, tags = Tags(tag -> 4), deprecated = true) - val actual = files.tag(file1, projectRef, tag, tagRev = 4, 5).accepted + val actual = files.tag(fileIdIri(file1), tag, tagRev = 4, 5).accepted actual shouldEqual expected } @@ -466,38 +470,38 @@ class FilesSpec(docker: RemoteStorageDocker) mkResource(file1, projectRef, diskRev, attributes(), rev = 6, tags = Tags(tag -> 4), deprecated = true) "succeed" in { - files.fetch(file1, projectRef).accepted shouldEqual resourceRev6 + files.fetch(fileIdIri(file1)).accepted shouldEqual resourceRev6 } "succeed by tag" in { - files.fetch(IdSegmentRef(file1, tag), projectRef).accepted shouldEqual resourceRev4 + files.fetch(FileId(file1, tag, projectRef)).accepted shouldEqual resourceRev4 } "succeed by rev" in { - files.fetch(IdSegmentRef(file1, 6), projectRef).accepted shouldEqual resourceRev6 - files.fetch(IdSegmentRef(file1, 1), projectRef).accepted shouldEqual resourceRev1 + files.fetch(FileId(file1, 6, projectRef)).accepted shouldEqual resourceRev6 + files.fetch(FileId(file1, 1, projectRef)).accepted shouldEqual resourceRev1 } "reject if tag does not exist" in { val otherTag = UserTag.unsafe("other") - files.fetch(IdSegmentRef(file1, otherTag), projectRef).rejected shouldEqual TagNotFound(otherTag) + files.fetch(FileId(file1, otherTag, projectRef)).rejected shouldEqual TagNotFound(otherTag) } "reject if revision does not exist" in { - files.fetch(IdSegmentRef(file1, 8), projectRef).rejected shouldEqual + files.fetch(FileId(file1, 8, projectRef)).rejected shouldEqual RevisionNotFound(provided = 8, current = 6) } "fail if it doesn't exist" in { val notFound = nxv + "notFound" - files.fetch(notFound, projectRef).rejectedWith[FileNotFound] - files.fetch(IdSegmentRef(notFound, tag), projectRef).rejectedWith[FileNotFound] - files.fetch(IdSegmentRef(notFound, 2), projectRef).rejectedWith[FileNotFound] + files.fetch(fileIdIri(notFound)).rejectedWith[FileNotFound] + files.fetch(FileId(notFound, tag, projectRef)).rejectedWith[FileNotFound] + files.fetch(FileId(notFound, 2, projectRef)).rejectedWith[FileNotFound] } "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - files.fetch(rdId, projectRef).rejectedWith[ProjectContextRejection] + files.fetch(FileId(rdId, projectRef)).rejectedWith[ProjectContextRejection] } } @@ -509,21 +513,21 @@ class FilesSpec(docker: RemoteStorageDocker) "fetching a file content" should { "succeed" in { - val response = files.fetchContent(file1, projectRef).accepted + val response = files.fetchContent(fileIdIri(file1)).accepted consumeContent(response) shouldEqual content response.metadata.filename shouldEqual "file.txt" response.metadata.contentType shouldEqual `text/plain(UTF-8)` } "succeed by tag" in { - val response = files.fetchContent(IdSegmentRef(file1, tag), projectRef).accepted + val response = files.fetchContent(FileId(file1, tag, projectRef)).accepted consumeContent(response) shouldEqual content response.metadata.filename shouldEqual "file.txt" response.metadata.contentType shouldEqual `text/plain(UTF-8)` } "succeed by rev" in { - val response = files.fetchContent(IdSegmentRef(file1, 1), projectRef).accepted + val response = files.fetchContent(FileId(file1, 1, projectRef)).accepted consumeContent(response) shouldEqual content response.metadata.filename shouldEqual "myfile.txt" response.metadata.contentType shouldEqual `text/plain(UTF-8)` @@ -531,24 +535,24 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if tag does not exist" in { val otherTag = UserTag.unsafe("other") - files.fetchContent(IdSegmentRef(file1, otherTag), projectRef).rejected shouldEqual TagNotFound(otherTag) + files.fetchContent(FileId(file1, otherTag, projectRef)).rejected shouldEqual TagNotFound(otherTag) } "reject if revision does not exist" in { - files.fetchContent(IdSegmentRef(file1, 8), projectRef).rejected shouldEqual + files.fetchContent(FileId(file1, 8, projectRef)).rejected shouldEqual RevisionNotFound(provided = 8, current = 6) } "fail if it doesn't exist" in { val notFound = nxv + "notFound" - files.fetchContent(notFound, projectRef).rejectedWith[FileNotFound] - files.fetchContent(IdSegmentRef(notFound, tag), projectRef).rejectedWith[FileNotFound] - files.fetchContent(IdSegmentRef(notFound, 2), projectRef).rejectedWith[FileNotFound] + files.fetchContent(fileIdIri(notFound)).rejectedWith[FileNotFound] + files.fetchContent(FileId(notFound, tag, projectRef)).rejectedWith[FileNotFound] + files.fetchContent(FileId(notFound, 2, projectRef)).rejectedWith[FileNotFound] } "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - files.fetchContent(rdId, projectRef).rejectedWith[ProjectContextRejection] + files.fetchContent(FileId(rdId, projectRef)).rejectedWith[ProjectContextRejection] } } diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala index 4dcf812fb3..c8779e0e15 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala @@ -16,11 +16,11 @@ import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit.scalatest.bio.BioSpec +import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec import java.time.Instant -class FilesStmSpec extends BioSpec with FileFixtures with StorageFixtures { +class FilesStmSpec extends CatsEffectSpec with FileFixtures with StorageFixtures { private val epoch = Instant.EPOCH private val time2 = Instant.ofEpochMilli(10L) 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 97561fe665..2688a8b95c 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 @@ -2,7 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files import akka.actor.ActorSystem import akka.http.scaladsl.model.ContentTypes._ -import akka.http.scaladsl.model.{ContentType, HttpCharsets, HttpEntity, MediaType, Multipart} +import akka.http.scaladsl.model._ import akka.testkit.TestKit import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF @@ -10,27 +10,19 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileDescription import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{FileTooLarge, InvalidMultipartFieldName} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.AkkaSourceHelpers import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ -import ch.epfl.bluebrain.nexus.testkit.scalatest.EitherValues -import ch.epfl.bluebrain.nexus.testkit.scalatest.bio.BIOValues -import monix.execution.Scheduler -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike +import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec import java.util.UUID class FormDataExtractorSpec extends TestKit(ActorSystem("FormDataExtractorSpec")) - with AnyWordSpecLike - with Matchers - with BIOValues - with EitherValues + with CatsEffectSpec with AkkaSourceHelpers { "A Form Data HttpEntity" should { - val uuid = UUID.randomUUID() - implicit val sc: Scheduler = Scheduler.global - implicit val uuidF: UUIDF = UUIDF.fixed(uuid) + val uuid = UUID.randomUUID() + implicit val uuidF: UUIDF = UUIDF.fixed(uuid) val content = "file content" val iri = iri"http://localhost/file" diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index c3830dbfce..6d20b7b6fd 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -9,7 +9,7 @@ import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.server.Route import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.Digest.ComputedDigest -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileRejection} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileId, FileRejection} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FilesRoutesSpec.fileMetadata import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{contexts => fileContexts, permissions, FileFixtures, Files, FilesConfig} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{StorageRejection, StorageStatEntry, StorageType} @@ -29,7 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.{Caller, ServiceAccount} import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegmentRef, ResourceUris} +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceUris} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.events import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy @@ -40,6 +40,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.testkit._ import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap +import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsIOValues import io.circe.Json import monix.bio.IO import org.scalatest._ @@ -49,7 +50,8 @@ class FilesRoutesSpec with CancelAfterFailure with StorageFixtures with FileFixtures - with IOFromMap { + with IOFromMap + with CatsIOValues { import akka.actor.typed.scaladsl.adapter._ implicit val typedSystem: typed.ActorSystem[Nothing] = system.toTyped @@ -109,16 +111,17 @@ class FilesRoutesSpec StoragesConfig(eventLogConfig, pagination, stCfg), ServiceAccount(User("nexus-sa", Label.unsafe("sa"))) ).accepted - lazy val files: Files = Files( - fetchContext.mapRejection(FileRejection.ProjectContextRejection), - aclCheck, - storages, - storagesStatistics, - xas, - config, - FilesConfig(eventLogConfig, MediaTypeDetectorConfig.Empty), - remoteDiskStorageClient - ) + lazy val files: Files = + Files( + fetchContext.mapRejection(FileRejection.ProjectContextRejection), + aclCheck, + storages, + storagesStatistics, + xas, + config, + FilesConfig(eventLogConfig, MediaTypeDetectorConfig.Empty), + remoteDiskStorageClient + )(ceClock, uuidF, contextShift, typedSystem) private val groupDirectives = DeltaSchemeDirectives(fetchContext, ioFromMap(uuid -> projectRef.organization), ioFromMap(uuid -> projectRef)) @@ -171,7 +174,7 @@ class FilesRoutesSpec status shouldEqual StatusCodes.Created val attr = attributes(id = uuid2) val expected = fileMetadata(projectRef, generatedId2, attr, diskIdRev) - val fileByTag = files.fetch(IdSegmentRef(generatedId2, tag), projectRef).accepted + val fileByTag = files.fetch(FileId(generatedId2, tag, projectRef)).accepted response.asJson shouldEqual expected fileByTag.value.tags.tags should contain(tag) } @@ -213,7 +216,7 @@ class FilesRoutesSpec status shouldEqual StatusCodes.Created val attr = attributes("fileTagged.txt", id = uuid2) val expected = fileMetadata(projectRef, fileTagged, attr, s3IdRev, createdBy = alice, updatedBy = alice) - val fileByTag = files.fetch(IdSegmentRef(generatedId2, tag), projectRef).accepted + val fileByTag = files.fetch(FileId(generatedId2, tag, projectRef)).accepted response.asJson shouldEqual expected fileByTag.value.tags.tags should contain(tag) } @@ -323,7 +326,7 @@ class FilesRoutesSpec } } - "reject the deprecation of a already deprecated file" in { + "reject the deprecation of an already deprecated file" in { Delete(s"/v1/files/org/proj/$uuid?rev=2") ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("/files/errors/file-deprecated.json", "id" -> generatedId) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclsImpl.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclsImpl.scala index 8dd3ea67ca..3d267c47e0 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclsImpl.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclsImpl.scala @@ -94,7 +94,7 @@ final class AclsImpl private ( private def eval(cmd: AclCommand): IO[AclRejection, AclResource] = log.evaluate(cmd.address, cmd).toBIO[AclRejection].map(_._2.toResource) - override def purge(project: AclAddress): UIO[Unit] = log.delete(project).toBIO + override def purge(project: AclAddress): UIO[Unit] = log.delete(project).toBIOThrowable } object AclsImpl { @@ -106,7 +106,7 @@ object AclsImpl { .listIds(Realms.entityType, xas.readCE) .compile .toList - .toBIO + .toBIOThrowable .flatMap { existing => val unknown = labels.filterNot { l => existing.contains(Realms.encodeId(l)) diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/BaseSpec.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/BaseSpec.scala index 726c454956..89fba1799e 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/BaseSpec.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/BaseSpec.scala @@ -5,7 +5,7 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.{Inspectors, OptionValues} -abstract class BaseSpec +trait BaseSpec extends AnyWordSpecLike with Matchers with EitherValues diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectSpec.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectSpec.scala index afbf2d933e..74a830570d 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectSpec.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectSpec.scala @@ -4,4 +4,4 @@ import ch.epfl.bluebrain.nexus.testkit.bio.IOFixedClock import ch.epfl.bluebrain.nexus.testkit.ce.CatsRunContext import ch.epfl.bluebrain.nexus.testkit.scalatest.BaseSpec -abstract class CatsEffectSpec extends BaseSpec with CatsRunContext with CatsIOValues with IOFixedClock +trait CatsEffectSpec extends BaseSpec with CatsRunContext with CatsIOValues with IOFixedClock