From 82ebce45bd0679b799dece735b13588447e8d54c Mon Sep 17 00:00:00 2001 From: Marcin Procyk Date: Tue, 19 Dec 2023 18:25:09 +0100 Subject: [PATCH] feat: Make Sipi handling optional during FileValue creation (Dev-2945) (#2960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Balduin Landolt <33053745+BalduinLandolt@users.noreply.github.com> Co-authored-by: Christian Kleinbölting --- .../testfiles/De6XyNL4H71-D9QxghOuOPJ.info | 7 + .../testfiles/De6XyNL4H71-D9QxghOuOPJ.jp2 | Bin 0 -> 32529 bytes .../De6XyNL4H71-D9QxghOuOPJ.png.orig | Bin 0 -> 47201 bytes .../org/knora/webapi/ITKnoraLiveSpec.scala | 4 + .../it/v2/KnoraSipiIntegrationV2ITSpec.scala | 40 +++- .../responders/v2/ValuesResponderV2Spec.scala | 8 +- .../store/iiif/impl/SipiServiceMock.scala | 6 +- .../testservices/TestClientService.scala | 3 + sipi/scripts/sipi.init.lua | 7 + .../resourcemessages/ResourceMessagesV2.scala | 23 +- .../valuemessages/ValueMessagesV2.scala | 225 ++++++++++++------ .../webapi/responders/v2/ResourceUtilV2.scala | 2 +- .../responders/v2/ResourcesResponderV2.scala | 21 +- .../webapi/routing/v2/ResourcesRouteV2.scala | 7 +- .../webapi/routing/v2/ValuesRouteV2.scala | 4 +- .../webapi/store/iiif/api/SipiService.scala | 16 +- .../store/iiif/impl/SipiServiceLive.scala | 35 ++- .../valuemessages/ValuesV2Spec.scala | 89 +++++++ 18 files changed, 384 insertions(+), 113 deletions(-) create mode 100644 integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.info create mode 100644 integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.jp2 create mode 100644 integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.png.orig create mode 100644 webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValuesV2Spec.scala diff --git a/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.info b/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.info new file mode 100644 index 0000000000..9b1745b97d --- /dev/null +++ b/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.info @@ -0,0 +1,7 @@ +{ + "originalInternalFilename": "De6XyNL4H71-D9QxghOuOPJ.png.orig", + "internalFilename": "De6XyNL4H71-D9QxghOuOPJ.jp2", + "checksumOriginal": "710aa7c06f7a7293f4cab70855ab98e166762a6b3ab7b6bdb4f6750bfc7028ed", + "originalFilename": "dog.png", + "checksumDerivative": "a89a2522adefe4510016ebdf337d82d3af0eef2a5c008c8c76553db4adb64155" +} \ No newline at end of file diff --git a/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.jp2 b/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.jp2 new file mode 100644 index 0000000000000000000000000000000000000000..4c74efcc2bdf911c8e64f9b8c1676e943cbc0cbd GIT binary patch literal 32529 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 Zkv=6cPzwzRZa5hKkpKh$7l8kg|JfY)YnA{2 literal 0 HcmV?d00001 diff --git a/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.png.orig b/integration/src/test/resources/sipi/testfiles/De6XyNL4H71-D9QxghOuOPJ.png.orig new file mode 100644 index 0000000000000000000000000000000000000000..ce15c9bb2e2de951b88da916ca89406759878eae GIT binary patch literal 47201 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/+TTr-we+;Yy5nA2`kbE|hmi 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 literal 0 HcmV?d00001 diff --git a/integration/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala b/integration/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala index 3a9d9bab44..179ad23799 100644 --- a/integration/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala @@ -31,6 +31,7 @@ import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtoc import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.routing.UnsafeZioRun +import org.knora.webapi.testcontainers.SipiTestContainer import org.knora.webapi.testservices.FileToUpload import org.knora.webapi.testservices.TestClientService import org.knora.webapi.util.LogAspect @@ -82,6 +83,9 @@ abstract class ITKnoraLiveSpec config <- ZIO.service[AppConfig] } yield (router, config) + def copyFileToImageFolderInContainer(prefix: String, filename: String) = + UnsafeZioRun.runOrThrow(SipiTestContainer.copyFileToImageFolderInContainer(prefix, filename)) + /** * Create router and config by unsafe running them. */ diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala index 9ef08dda7c..1cd59a8f75 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala @@ -5,7 +5,9 @@ package org.knora.webapi.it.v2 -import org.apache.pekko +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials +import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal import java.net.URLEncoder import java.nio.file.Paths @@ -30,10 +32,6 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.testservices.FileToUpload import org.knora.webapi.util.MutableTestIri -import pekko.http.scaladsl.model._ -import pekko.http.scaladsl.model.headers.BasicHttpCredentials -import pekko.http.scaladsl.unmarshalling.Unmarshal - /** * Tests interaction between Knora and Sipi using Knora API v2. */ @@ -505,6 +503,38 @@ class KnoraSipiIntegrationV2ITSpec assert(savedImage.internalFilename == uploadedFile.filename) } + "create a resource with a still image file that has already been ingested" in { + copyFileToImageFolderInContainer("0001", "De6XyNL4H71-D9QxghOuOPJ.jp2") + copyFileToImageFolderInContainer("0001", "De6XyNL4H71-D9QxghOuOPJ.info") + copyFileToImageFolderInContainer("0001", "De6XyNL4H71-D9QxghOuOPJ.png.orig") + // Create the resource in the API. + val jsonLdEntity = UploadFileRequest + .make(fileType = FileType.StillImageFile(), internalFilename = "De6XyNL4H71-D9QxghOuOPJ.jp2") + .toJsonLd(className = Some("ThingPicture"), ontologyName = "anything") + val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> + addCredentials(BasicHttpCredentials(anythingUserEmail, password)) ~> + addHeader("X-Asset-Ingested", "true") + val responseJsonDoc: JsonLDDocument = getResponseJsonLD(request) + // Get the resource from the API. + val resIri = UnsafeZioRun.runOrThrow(responseJsonDoc.body.getRequiredIdValueAsKnoraDataIri).toString + val getRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resIri, "UTF-8")}") + checkResponseOK(getRequest) + } + + "not create a resource with a still image file that has already been ingested if the header is not provided" in { + copyFileToImageFolderInContainer("0001", "De6XyNL4H71-D9QxghOuOPJ.jp2") + copyFileToImageFolderInContainer("0001", "De6XyNL4H71-D9QxghOuOPJ.info") + copyFileToImageFolderInContainer("0001", "De6XyNL4H71-D9QxghOuOPJ.png.orig") + // Create the resource in the API. + val jsonLdEntity = UploadFileRequest + .make(fileType = FileType.StillImageFile(), internalFilename = "De6XyNL4H71-D9QxghOuOPJ.jp2") + .toJsonLd(className = Some("ThingPicture"), ontologyName = "anything") + val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> + addCredentials(BasicHttpCredentials(anythingUserEmail, password)) // no X-Asset-Ingested header + val res = singleAwaitingRequest(request) + assert(res.status == StatusCodes.BadRequest) + } + "reject an image file with the wrong file extension" in { val exception = intercept[BadRequestException] { uploadToSipi( diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index b7e2c2330f..cce7711c34 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -5,7 +5,7 @@ package org.knora.webapi.responders.v2 -import org.apache.pekko +import org.apache.pekko.testkit.ImplicitSender import java.time.Instant import java.util.UUID @@ -26,6 +26,7 @@ import org.knora.webapi.messages.util.DatePrecisionYear import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.messages.util.PermissionUtilADM import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.messages.v2.responder.valuemessages._ @@ -39,8 +40,6 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select import org.knora.webapi.util.MutableTestIri import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA -import pekko.testkit.ImplicitSender - /** * Tests [[ValuesResponderV2]]. */ @@ -457,7 +456,8 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { val resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri val propertyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri val intVal = IntegerValueContentV2(ApiV2Complex, 4) - val duplicateValue = CreateValueV2(resourceIri, resourceClassIri, propertyIri, intVal) + val duplicateValue = + CreateValueV2(resourceIri, resourceClassIri, propertyIri, intVal, ingestState = AssetIngestState.AssetInTemp) val actual = UnsafeZioRun.run(ValuesResponderV2.createValueV2(duplicateValue, anythingUser1, randomUUID)) assertFailsWithA[DuplicateValueException](actual) diff --git a/integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala b/integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala index 503f814ae3..d105796e0b 100644 --- a/integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala +++ b/integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala @@ -11,6 +11,7 @@ import zio.nio.file.Path import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages._ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.service.Asset import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse import org.knora.webapi.store.iiif.api.SipiService @@ -27,7 +28,7 @@ case class SipiServiceMock() extends SipiService { */ private val FAILURE_FILENAME: String = "failure.jp2" - override def getFileMetadata(ignoredByMock: String): Task[FileMetadataSipiResponse] = + override def getFileMetadataFromTemp(filename: String): Task[FileMetadataSipiResponse] = ZIO.succeed( FileMetadataSipiResponse( originalFilename = Some("test2.tiff"), @@ -62,6 +63,9 @@ case class SipiServiceMock() extends SipiService { override def getStatus(): Task[IIIFServiceStatusResponse] = ZIO.succeed(IIIFServiceStatusOK) override def downloadAsset(asset: Asset, targetDir: Path, user: UserADM): Task[Option[Path]] = ??? + + override def getFileMetadata(filename: String, shortcode: KnoraProject.Shortcode): Task[FileMetadataSipiResponse] = + ??? } object SipiServiceMock { diff --git a/integration/src/test/scala/org/knora/webapi/testservices/TestClientService.scala b/integration/src/test/scala/org/knora/webapi/testservices/TestClientService.scala index 31f9f72a77..cb4ee3c467 100644 --- a/integration/src/test/scala/org/knora/webapi/testservices/TestClientService.scala +++ b/integration/src/test/scala/org/knora/webapi/testservices/TestClientService.scala @@ -148,6 +148,9 @@ final case class TestClientService(config: AppConfig, httpClient: CloseableHttpC .mapError(error => throw AssertionException(s"Got HTTP ${response.status.intValue}\n REQUEST: $request, \n RESPONSE: $error") ) + _ <- ZIO + .fail(AssertionException(s"Got HTTP ${response.status.intValue}\n REQUEST: $request, \n RESPONSE: $body")) + .when(response.status.isFailure()) } yield body /** diff --git a/sipi/scripts/sipi.init.lua b/sipi/scripts/sipi.init.lua index 051fa336b7..5890d8a0d6 100644 --- a/sipi/scripts/sipi.init.lua +++ b/sipi/scripts/sipi.init.lua @@ -96,11 +96,18 @@ function pre_flight(prefix, identifier, cookie) end log("pre_flight - filepath: " .. filepath, server.loglevel.LOG_DEBUG) + if prefix == "tmp" then log("pre_flight - always allow access to tmp folder", server.loglevel.LOG_DEBUG) return 'allow', filepath end + local token, error = auth_get_jwt_decoded() + if error == nil and token ~= nil and token["sub"] == "http://www.knora.org/ontology/knora-admin#SystemUser" then + log("pre_flight - always allow access for system user", server.loglevel.LOG_DEBUG) + return 'allow', filepath + end + local jwt_raw = auth_get_jwt_raw() local permission_info = get_permission_on_file(prefix, identifier, jwt_raw) if permission_info == nil then diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 2961af50f6..70596b2d46 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -34,6 +34,8 @@ import org.knora.webapi.messages.util.rdf.* import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.messages.util.standoff.XMLUtil import org.knora.webapi.messages.v2.responder.* +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState.AssetInTemp import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -656,10 +658,16 @@ case class CreateResourceV2( case class CreateResourceRequestV2( createResource: CreateResourceV2, requestingUser: UserADM, - apiRequestID: UUID + apiRequestID: UUID, + ingestState: AssetIngestState = AssetInTemp ) extends ResourcesResponderRequestV2 object CreateResourceRequestV2 { + sealed trait AssetIngestState + object AssetIngestState { + case object AssetIngested extends AssetIngestState + case object AssetInTemp extends AssetIngestState + } /** * Converts JSON-LD input to a [[CreateResourceRequestV2]]. @@ -667,12 +675,14 @@ object CreateResourceRequestV2 { * @param jsonLDDocument the JSON-LD input. * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. + * @param ingestState indicates the state of the file, either ingested or in temp folder * @return a case class instance representing the input. */ def fromJsonLd( jsonLDDocument: JsonLDDocument, apiRequestID: UUID, - requestingUser: UserADM + requestingUser: UserADM, + ingestState: AssetIngestState = AssetInTemp ): ZIO[IriConverter & SipiService & StringFormatter & MessageRelay, Throwable, CreateResourceRequestV2] = ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => val validationFun: (String, => Nothing) => String = @@ -775,8 +785,12 @@ object CreateResourceRequestV2 { ) ) } + fileInfo <- ValueContentV2 + .getFileInfo(projectInfoResponse.project.shortcode, ingestState, valueJsonLDObject) + .option - valueContent <- ValueContentV2.fromJsonLdObject(valueJsonLDObject, requestingUser) + valueContent <- + ValueContentV2.fromJsonLdObject(ingestState, valueJsonLDObject, requestingUser, fileInfo) maybeCustomValueIri <- valueJsonLDObject.getIdValueAsKnoraDataIri .mapError(BadRequestException(_)) @@ -841,7 +855,8 @@ object CreateResourceRequestV2 { creationDate = creationDate ), requestingUser = maybeAttachedToUser.getOrElse(requestingUser), - apiRequestID = apiRequestID + apiRequestID = apiRequestID, + ingestState = ingestState ) } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 9bcd3e283a..2465064ffc 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -12,6 +12,7 @@ import java.util.UUID import dsp.errors.AssertionException import dsp.errors.BadRequestException +import dsp.errors.NotFoundException import dsp.errors.NotImplementedException import dsp.valueobjects.Iri import dsp.valueobjects.IriErrorMessages @@ -37,10 +38,13 @@ import org.knora.webapi.messages.util.standoff.StandoffStringUtil import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.messages.util.standoff.XMLUtil import org.knora.webapi.messages.v2.responder.* +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState.AssetInTemp import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages.* import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilZ +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse import org.knora.webapi.store.iiif.api.SipiService @@ -583,6 +587,7 @@ case class ReadOtherValueV2( /** * Represents a Knora value to be created in an existing resource. * + * @param ingestState indicates the state of the file, either ingested or in temp folder * @param resourceIri the resource the new value should be attached to. * @param resourceClassIri the resource class that the client believes the resource belongs to. * @param propertyIri the property of the new value. If the client wants to create a link, this must be a link value property. @@ -601,7 +606,8 @@ case class CreateValueV2( valueIri: Option[SmartIri] = None, valueUUID: Option[UUID] = None, valueCreationDate: Option[Instant] = None, - permissions: Option[String] = None + permissions: Option[String] = None, + ingestState: AssetIngestState = AssetInTemp ) extends IOValueV2 /** @@ -612,11 +618,13 @@ object CreateValueV2 { /** * Converts JSON-LD input to a [[CreateValueV2]]. * + * @param ingestState indicates the state of the file, either ingested or in temp folder * @param jsonLdString JSON-LD input as String. * @param requestingUser the user making the request. * @return a case class instance representing the input. */ def fromJsonLd( + ingestState: AssetIngestState, jsonLdString: String, requestingUser: UserADM ): ZIO[SipiService & StringFormatter & IriConverter & MessageRelay, Throwable, CreateValueV2] = @@ -628,6 +636,8 @@ object CreateValueV2 { .mapError(BadRequestException(_)) .flatMap(RouteUtilZ.ensureIsKnoraResourceIri) + shortcode <- ZIO.fromOption(resourceIri.getProjectCode).orElseFail(NotFoundException("Shortcode not found.")) + // Get the resource class. resourceClassIri <- jsonLDDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) @@ -637,7 +647,9 @@ object CreateValueV2 { jsonLDDocument.body.getRequiredResourcePropertyApiV2ComplexValue.mapError(BadRequestException(_)).flatMap { case (propertyIri: SmartIri, jsonLdObject: JsonLDObject) => for { - valueContent <- ValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) + fileInfo <- ValueContentV2.getFileInfo(shortcode, ingestState, jsonLdObject).option + valueContent <- + ValueContentV2.fromJsonLdObject(ingestState, jsonLdObject, requestingUser, fileInfo) // Get and validate the custom value IRI if provided. maybeCustomValueIri <- jsonLdObject.getIdValueAsKnoraDataIri @@ -646,7 +658,7 @@ object CreateValueV2 { definedNewIri.foreach( stringFormatter.validateCustomValueIri( _, - resourceIri.getProjectCode.get, + shortcode, resourceIri.getResourceID.get ) ) @@ -691,7 +703,8 @@ object CreateValueV2 { valueIri = maybeCustomValueIri, valueUUID = maybeCustomUUID, valueCreationDate = maybeCreationDate, - permissions = maybePermissions + permissions = maybePermissions, + ingestState = ingestState ) } } yield createValue @@ -752,13 +765,21 @@ object UpdateValueV2 { maybeNewIri: Option[SmartIri] ) = for { - valueContent <- ValueContentV2.fromJsonLdObject(jsonLDObject, requestingUser) maybePermissions <- ZIO.attempt { val validationFun: (String, => Nothing) => String = (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) jsonLDObject.maybeStringWithValidation(HasPermissions, validationFun) } + shortcode <- ZIO.fromOption(resourceIri.getProjectCode).orElseFail(NotFoundException("Shortcode not found.")) + fileInfo <- ValueContentV2.getFileInfo(shortcode, AssetIngestState.AssetInTemp, jsonLDObject).option + valueContent <- + ValueContentV2.fromJsonLdObject( + AssetIngestState.AssetInTemp, + jsonLDObject, + requestingUser, + fileInfo + ) } yield UpdateValueContentV2( resourceIri = resourceIri.toString, resourceClassIri = resourceClassIri, @@ -1041,14 +1062,17 @@ object ValueContentV2 { /** * Converts a JSON-LD object to a [[ValueContentV2]]. * + * @param ingestState indicates the state of the file, either ingested or in temp folder * @param jsonLdObject the JSON-LD object. * @param requestingUser the user making the request. * @return a [[ValueContentV2]]. */ def fromJsonLdObject( + ingestState: AssetIngestState, jsonLdObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[SipiService & StringFormatter & MessageRelay, Throwable, ValueContentV2] = + requestingUser: UserADM, + fileInfo: Option[FileInfo] + ): ZIO[StringFormatter & MessageRelay, Throwable, ValueContentV2] = ZIO.serviceWithZIO[StringFormatter] { stringFormatter => for { valueType <- @@ -1069,17 +1093,67 @@ object ValueContentV2 { case UriValue => UriValueContentV2.fromJsonLdObject(jsonLdObject) case GeonameValue => GeonameValueContentV2.fromJsonLdObject(jsonLdObject) case ColorValue => ColorValueContentV2.fromJsonLdObject(jsonLdObject) - case StillImageFileValue => StillImageFileValueContentV2.fromJsonLdObject(jsonLdObject) - case DocumentFileValue => DocumentFileValueContentV2.fromJsonLdObject(jsonLdObject) - case TextFileValue => TextFileValueContentV2.fromJsonLdObject(jsonLdObject) - case AudioFileValue => AudioFileValueContentV2.fromJsonLdObject(jsonLdObject) - case MovingImageFileValue => MovingImageFileValueContentV2.fromJsonLdObject(jsonLdObject) - case ArchiveFileValue => ArchiveFileValueContentV2.fromJsonLdObject(jsonLdObject) - case other => ZIO.fail(NotImplementedException(s"Parsing of JSON-LD value type not implemented: $other")) + case StillImageFileValue => + for { + info <- + ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for StillImageFileValue")) + content <- StillImageFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata) + } yield content + case DocumentFileValue => + for { + info <- + ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for DocumentFileValue")) + content <- DocumentFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata) + } yield content + case TextFileValue => + for { + info <- ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for TextFileValue")) + content <- TextFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata) + } yield content + case AudioFileValue => + for { + info <- + ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for AudioFileValue")) + content <- AudioFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata) + } yield content + case MovingImageFileValue => + for { + info <- ZIO + .fromOption(fileInfo) + .orElseFail(BadRequestException("No file info found for MovingImageFileValue")) + content <- MovingImageFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata) + } yield content + case ArchiveFileValue => + for { + info <- + ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for ArchiveFileValue")) + content <- ArchiveFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata) + } yield content + case other => ZIO.fail(NotImplementedException(s"Parsing of JSON-LD value type not implemented: $other")) } - } yield valueContent } + + final case class FileInfo(filename: IRI, metadata: FileMetadataSipiResponse) + + def getFileInfo( + shortcode: String, + ingestState: AssetIngestState, + jsonLdObject: JsonLDObject + ): ZIO[SipiService, Throwable, FileInfo] = + for { + internalFilename <- ZIO.attempt { + val validationFun: (IRI, => Nothing) => IRI = + (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) + jsonLdObject.requireStringWithValidation(FileValueHasFilename, validationFun) + } + metadata <- ingestState match { + case AssetIngestState.AssetIngested => + SipiService.getFileMetadata(internalFilename, Shortcode.unsafeFrom(shortcode)) + case AssetIngestState.AssetInTemp => SipiService.getFileMetadataFromTemp(internalFilename) + } + + } yield FileInfo(internalFilename, metadata) } /** @@ -2565,33 +2639,6 @@ case class FileValueV2( originalMimeType: Option[String] ) -/** - * Holds a [[FileValueV2]] and the metadata that Sipi returned about the file. - * - * @param fileValue a [[FileValueV2]]. - * @param sipiFileMetadata the metadata that Sipi returned about the file. - */ -case class FileValueWithSipiMetadata(fileValue: FileValueV2, sipiFileMetadata: FileMetadataSipiResponse) - -/** - * Constructs [[FileValueWithSipiMetadata]] objects based on JSON-LD input. - */ -object FileValueWithSipiMetadata { - def fromJsonLdObject( - jsonLDObject: JsonLDObject - ): ZIO[SipiService, Throwable, FileValueWithSipiMetadata] = - for { - // The submitted value provides only Sipi's internal filename for the file. - internalFilename <- ZIO.attempt { - val validationFun: (String, => Nothing) => String = - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) - jsonLDObject.requireStringWithValidation(FileValueHasFilename, validationFun) - } - meta <- SipiService.getFileMetadata(s"/tmp/$internalFilename") - fileValue = FileValueV2(internalFilename, meta.internalMimeType, meta.originalFilename, meta.originalMimeType) - } yield FileValueWithSipiMetadata(fileValue, meta) -} - /** * A trait for case classes representing different types of file values. */ @@ -2702,16 +2749,18 @@ case class StillImageFileValueContentV2( */ object StillImageFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject - ): ZIO[SipiService & StringFormatter, Throwable, StillImageFileValueContentV2] = + jsonLDObject: JsonLDObject, + internalFilename: String, + metadata: FileMetadataSipiResponse + ): ZIO[StringFormatter, Throwable, StillImageFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) - comment <- JsonLDUtil.getComment(jsonLDObject) + comment <- JsonLDUtil.getComment(jsonLDObject) } yield StillImageFileValueContentV2( ontologySchema = ApiV2Complex, - fileValue = fileValueWithSipiMetadata.fileValue, - dimX = fileValueWithSipiMetadata.sipiFileMetadata.width.getOrElse(0), - dimY = fileValueWithSipiMetadata.sipiFileMetadata.height.getOrElse(0), + fileValue = + FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType), + dimX = metadata.width.getOrElse(0), + dimY = metadata.height.getOrElse(0), comment = comment ) } @@ -2850,17 +2899,19 @@ case class ArchiveFileValueContentV2( */ object DocumentFileValueContentV2 { def fromJsonLdObject( - jsonLdObject: JsonLDObject - ): ZIO[SipiService & StringFormatter, Throwable, DocumentFileValueContentV2] = + jsonLdObject: JsonLDObject, + internalFilename: String, + metadata: FileMetadataSipiResponse + ): ZIO[StringFormatter, Throwable, DocumentFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLdObject) - comment <- JsonLDUtil.getComment(jsonLdObject) + comment <- JsonLDUtil.getComment(jsonLdObject) } yield DocumentFileValueContentV2( ontologySchema = ApiV2Complex, - fileValue = fileValueWithSipiMetadata.fileValue, - pageCount = fileValueWithSipiMetadata.sipiFileMetadata.numpages, - dimX = fileValueWithSipiMetadata.sipiFileMetadata.width, - dimY = fileValueWithSipiMetadata.sipiFileMetadata.height, + fileValue = + FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType), + pageCount = metadata.numpages, + dimX = metadata.width, + dimY = metadata.height, comment ) } @@ -2870,12 +2921,17 @@ object DocumentFileValueContentV2 { */ object ArchiveFileValueContentV2 { def fromJsonLdObject( - jsonLdObject: JsonLDObject - ): ZIO[SipiService & StringFormatter, Throwable, ArchiveFileValueContentV2] = + jsonLdObject: JsonLDObject, + internalFilename: String, + metadata: FileMetadataSipiResponse + ): ZIO[StringFormatter, Throwable, ArchiveFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLdObject) - comment <- JsonLDUtil.getComment(jsonLdObject) - } yield ArchiveFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) + comment <- JsonLDUtil.getComment(jsonLdObject) + } yield ArchiveFileValueContentV2( + ApiV2Complex, + FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType), + comment + ) } /** @@ -2942,11 +2998,16 @@ case class TextFileValueContentV2( */ object TextFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject - ): ZIO[SipiService & StringFormatter, Throwable, TextFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) - comment <- JsonLDUtil.getComment(jsonLDObject) - } yield TextFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) + jsonLDObject: JsonLDObject, + internalFilename: String, + metadata: FileMetadataSipiResponse + ): ZIO[StringFormatter, Throwable, TextFileValueContentV2] = for { + comment <- JsonLDUtil.getComment(jsonLDObject) + } yield TextFileValueContentV2( + ApiV2Complex, + FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType), + comment + ) } /** @@ -3013,12 +3074,17 @@ case class AudioFileValueContentV2( */ object AudioFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject - ): ZIO[SipiService & StringFormatter, Throwable, AudioFileValueContentV2] = + jsonLDObject: JsonLDObject, + internalFilename: String, + metadata: FileMetadataSipiResponse + ): ZIO[StringFormatter, Throwable, AudioFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) - comment <- JsonLDUtil.getComment(jsonLDObject) - } yield AudioFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) + comment <- JsonLDUtil.getComment(jsonLDObject) + } yield AudioFileValueContentV2( + ApiV2Complex, + FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType), + comment + ) } /** @@ -3087,12 +3153,17 @@ case class MovingImageFileValueContentV2( */ object MovingImageFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject - ): ZIO[SipiService & StringFormatter, Throwable, MovingImageFileValueContentV2] = + jsonLDObject: JsonLDObject, + internalFilename: String, + metadata: FileMetadataSipiResponse + ): ZIO[StringFormatter, Throwable, MovingImageFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) - comment <- JsonLDUtil.getComment(jsonLDObject) - } yield MovingImageFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) + comment <- JsonLDUtil.getComment(jsonLDObject) + } yield MovingImageFileValueContentV2( + ApiV2Complex, + FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType), + comment + ) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala index 6425c06c2e..103bafce00 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala @@ -108,7 +108,7 @@ trait ResourceUtilV2 { fileValues: Seq[FileValueContentV2], requestingUser: UserADM ): Task[T] - def doSipiPostUpdate[T <: UpdateResultInProject]( + final def doSipiPostUpdate[T <: UpdateResultInProject]( updateTask: Task[T], fileValue: FileValueContentV2, requestingUser: UserADM diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 0bfce732d2..e3c300096f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -44,6 +44,7 @@ import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.* import org.knora.webapi.messages.v2.responder.ontologymessages.* +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingResponseV2 @@ -369,14 +370,18 @@ final case class ResourcesResponderV2Live( ) } yield taskResult - // If the request includes file values, tell Sipi to move the files to permanent storage if the update - // succeeded, or to delete the temporary files if the update failed. - val fileValues = Seq(createResourceRequestV2.createResource) - .flatMap(_.flatValues) - .map(_.valueContent) - .filter(_.isInstanceOf[FileValueContentV2]) - .map(_.asInstanceOf[FileValueContentV2]) - resourceUtilV2.doSipiPostUpdate(triplestoreUpdateFuture, fileValues, createResourceRequestV2.requestingUser) + createResourceRequestV2.ingestState match { + case AssetIngestState.AssetIngested => triplestoreUpdateFuture + // If the request includes file values, tell Sipi to move the files to permanent storage if the update + // succeeded, or to delete the temporary files if the update failed. + case AssetIngestState.AssetInTemp => + val fileValues = Seq(createResourceRequestV2.createResource) + .flatMap(_.flatValues) + .map(_.valueContent) + .filter(_.isInstanceOf[FileValueContentV2]) + .map(_.asInstanceOf[FileValueContentV2]) + resourceUtilV2.doSipiPostUpdate(triplestoreUpdateFuture, fileValues, createResourceRequestV2.requestingUser) + } } /** diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 5fe9811db8..8bdf0ffa1d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -26,6 +26,8 @@ import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.ValuesValidator.arkTimestampToInstant import org.knora.webapi.messages.ValuesValidator.xsdDateTimeStampToInstant import org.knora.webapi.messages.util.rdf.JsonLDUtil +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState.AssetInTemp +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState.AssetIngested import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.v2.SearchResponderV2 @@ -99,7 +101,10 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( requestDoc <- RouteUtilV2.parseJsonLd(jsonRequest) requestingUser <- Authenticator.getUserADM(requestContext) apiRequestId <- RouteUtilZ.randomUuid() - requestMessage <- CreateResourceRequestV2.fromJsonLd(requestDoc, apiRequestId, requestingUser) + header = "X-Asset-Ingested" + ingestState = if (requestContext.request.headers.exists(_.name == header)) AssetIngested + else AssetInTemp + requestMessage <- CreateResourceRequestV2.fromJsonLd(requestDoc, apiRequestId, requestingUser, ingestState) // check for each value which represents a file value if the file's MIME type is allowed _ <- checkMimeTypesForFileValueContents(requestMessage.createResource.flatValues) } yield requestMessage diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala index eb09dc23d4..72d4c0e61d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala @@ -16,6 +16,8 @@ import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState.* import org.knora.webapi.messages.v2.responder.resourcemessages.ResourcesGetRequestV2 import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.v2.ValuesResponderV2 @@ -83,7 +85,7 @@ final case class ValuesRouteV2()( for { requestingUser <- Authenticator.getUserADM(ctx) apiRequestId <- Random.nextUUID - valueToCreate <- CreateValueV2.fromJsonLd(jsonLdString, requestingUser) + valueToCreate <- CreateValueV2.fromJsonLd(AssetIngestState.AssetInTemp, jsonLdString, requestingUser) response <- ValuesResponderV2.createValueV2(valueToCreate, requestingUser, apiRequestId) } yield response, ctx diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala index f7db7e776a..4be4349138 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala @@ -14,6 +14,7 @@ import zio.nio.file.Path import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages.* import org.knora.webapi.messages.v2.responder.SuccessResponseV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.service.Asset import org.knora.webapi.store.iiif.errors.SipiException @@ -62,12 +63,21 @@ object FileMetadataSipiResponse { trait SipiService { /** - * Asks Sipi for metadata about a file, served from the 'knora.json' route. + * Asks Sipi for metadata about a file in the tmp folder, served from the 'knora.json' route. * - * @param filePath the path to the file. + * @param filename the path to the file. * @return a [[FileMetadataSipiResponse]] containing the requested metadata. */ - def getFileMetadata(filePath: String): Task[FileMetadataSipiResponse] + def getFileMetadataFromTemp(filename: String): Task[FileMetadataSipiResponse] + + /** + * Asks Sipi for metadata about a file in permanent location, served from the 'knora.json' route. + * + * @param filename the path to the file. + * @param shortcode the shortcode of the project. + * @return a [[FileMetadataSipiResponse]] containing the requested metadata. + */ + def getFileMetadata(filename: String, shortcode: Shortcode): Task[FileMetadataSipiResponse] /** * Asks Sipi to move a file from temporary storage to permanent storage. diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala index 1c17ed960d..85f50d1c30 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala @@ -19,6 +19,7 @@ import org.apache.http.config.SocketConfig import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.impl.client.HttpClients import org.apache.http.impl.conn.PoolingHttpClientConnectionManager +import org.apache.http.message.BasicHeader import org.apache.http.message.BasicNameValuePair import org.apache.http.util.EntityUtils import spray.json.* @@ -35,9 +36,11 @@ import org.knora.webapi.config.AppConfig import org.knora.webapi.config.Sipi import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages.* +import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.routing.Jwt import org.knora.webapi.routing.JwtService +import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.service.Asset import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse import org.knora.webapi.store.iiif.api.SipiService @@ -69,16 +72,32 @@ final case class SipiServiceLive( /** * Asks Sipi for metadata about a file, served from the 'knora.json' route. * - * @param filePath the path to the file. + * @param filename the file name * @return a [[FileMetadataSipiResponse]] containing the requested metadata. */ - override def getFileMetadata(filePath: String): Task[FileMetadataSipiResponse] = - doSipiRequest(new HttpGet(sipiConfig.internalBaseUrl + filePath + "/knora.json")) - .flatMap(bodyStr => - ZIO - .fromEither(bodyStr.fromJson[FileMetadataSipiResponse]) - .mapError(e => SipiException(s"Invalid response from Sipi: $e, $bodyStr")) - ) + override def getFileMetadataFromTemp(filename: String): Task[FileMetadataSipiResponse] = + getFileMetadataFromUrl(s"${sipiConfig.internalBaseUrl}/tmp/$filename/knora.json") + + /** + * Asks Sipi for metadata about a file in permanent location, served from the 'knora.json' route. + * + * @param filename the path to the file. + * @param shortcode the shortcode of the project. + * @return a [[FileMetadataSipiResponse]] containing the requested metadata. + */ + override def getFileMetadata(filename: String, shortcode: KnoraProject.Shortcode): Task[FileMetadataSipiResponse] = + getFileMetadataFromUrl(s"${sipiConfig.internalBaseUrl}/${shortcode.value}/$filename/knora.json") + + private def getFileMetadataFromUrl(url: String): Task[FileMetadataSipiResponse] = + for { + jwt <- jwtService.createJwt(KnoraSystemInstances.Users.SystemUser) + request = new HttpGet(url) + _ = request.addHeader(new BasicHeader("Authorization", s"Bearer ${jwt.jwtString}")) + bodyStr <- doSipiRequest(request) + res <- ZIO + .fromEither(bodyStr.fromJson[FileMetadataSipiResponse]) + .mapError(e => SipiException(s"Invalid response from Sipi: $e, $bodyStr")) + } yield res /** * Asks Sipi to move a file from temporary storage to permanent storage. diff --git a/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValuesV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValuesV2Spec.scala new file mode 100644 index 0000000000..ba04e8071c --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValuesV2Spec.scala @@ -0,0 +1,89 @@ +/* + * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.messages.v2.responder.valuemessages + +import zio.* +import zio.nio.file.Path +import zio.test.Assertion.* +import zio.test.* + +import dsp.errors.AssertionException +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.messages.store.sipimessages.* +import org.knora.webapi.messages.util.rdf.JsonLDUtil +import org.knora.webapi.messages.v2.responder.SuccessResponseV2 +import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2.AssetIngestState +import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.service.Asset +import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse +import org.knora.webapi.store.iiif.api.SipiService + +object ValuesV2Spec extends ZIOSpecDefault { + private val json = + """{ + | "http://api.knora.org/ontology/knora-api/v2#fileValueHasFilename" : "filename" + | }""".stripMargin + + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("ValuesV2")( + test("Expect file to be not ingested and in `tmp` folder") { + for { + ingested <- ValueContentV2 + .getFileInfo("0001", AssetIngestState.AssetIngested, JsonLDUtil.parseJsonLD(json).body) + .exit + temp <- ValueContentV2 + .getFileInfo("0001", AssetIngestState.AssetInTemp, JsonLDUtil.parseJsonLD(json).body) + .exit + } yield assert(ingested)(fails(isSubtype[AssertionException](anything))) && assert(temp)(succeeds(anything)) + }.provide(sipiServiceAllowMetadataInTemp), + test("Expect file to be already ingested and in project folder") { + for { + ingested <- ValueContentV2 + .getFileInfo("0001", AssetIngestState.AssetIngested, JsonLDUtil.parseJsonLD(json).body) + .exit + temp <- ValueContentV2 + .getFileInfo("0001", AssetIngestState.AssetInTemp, JsonLDUtil.parseJsonLD(json).body) + .exit + } yield assert(ingested)(succeeds(anything)) && assert(temp)(fails(isSubtype[AssertionException](anything))) + }.provide(sipiServiceAllowMetadataInProjectFolder) + ) + + private val response: FileMetadataSipiResponse = + FileMetadataSipiResponse(None, None, "", None, None, None, None, None) + + private def sipiServiceAllowMetadataInTemp: ULayer[SipiService] = ZLayer.succeed(new SipiService { + override def getFileMetadataFromTemp(filename: String): Task[FileMetadataSipiResponse] = + ZIO.succeed(response) + override def getFileMetadata(filename: String, shortcode: KnoraProject.Shortcode): Task[FileMetadataSipiResponse] = + ZIO.fail(AssertionException(filename)) + def moveTemporaryFileToPermanentStorage( + moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest + ): Task[SuccessResponseV2] = ??? + def deleteTemporaryFile( + deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest + ): Task[SuccessResponseV2] = ??? + def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] = ??? + def getStatus(): Task[IIIFServiceStatusResponse] = ??? + def downloadAsset(asset: Asset, targetDir: Path, user: UserADM): Task[Option[Path]] = ??? + }) + + private def sipiServiceAllowMetadataInProjectFolder: ULayer[SipiService] = ZLayer.succeed(new SipiService { + override def getFileMetadata(filename: String, shortcode: KnoraProject.Shortcode): Task[FileMetadataSipiResponse] = + ZIO.succeed(response) + override def getFileMetadataFromTemp(filename: String): Task[FileMetadataSipiResponse] = + ZIO.fail(AssertionException(filename)) + def moveTemporaryFileToPermanentStorage( + moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest + ): Task[SuccessResponseV2] = ??? + def deleteTemporaryFile( + deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest + ): Task[SuccessResponseV2] = ??? + def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] = ??? + def getStatus(): Task[IIIFServiceStatusResponse] = ??? + def downloadAsset(asset: Asset, targetDir: Path, user: UserADM): Task[Option[Path]] = ??? + }) + +}