Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement custom media type detection #4312

Merged
merged 10 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 6 additions & 21 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ val akkaCorsVersion = "1.2.0"
val akkaVersion = "2.6.21"
val alpakkaVersion = "3.0.4"
val apacheCompressVersion = "1.24.0"
val apacheIoVersion = "1.3.2"
val awsSdkVersion = "2.17.184"
val byteBuddyAgentVersion = "1.10.17"
val betterMonadicForVersion = "0.3.1"
Expand Down Expand Up @@ -77,7 +76,6 @@ lazy val alpakkaFile = "com.lightbend.akka" %% "akka-stream-alp
lazy val alpakkaSse = "com.lightbend.akka" %% "akka-stream-alpakka-sse" % alpakkaVersion
lazy val alpakkaS3 = "com.lightbend.akka" %% "akka-stream-alpakka-s3" % alpakkaVersion
lazy val apacheCompress = "org.apache.commons" % "commons-compress" % apacheCompressVersion
lazy val apacheIo = "org.apache.commons" % "commons-io" % apacheIoVersion
lazy val awsSdk = "software.amazon.awssdk" % "s3" % awsSdkVersion
lazy val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % betterMonadicForVersion
lazy val byteBuddyAgent = "net.bytebuddy" % "byte-buddy-agent" % byteBuddyAgentVersion
Expand Down Expand Up @@ -206,6 +204,8 @@ lazy val kernel = project
.settings(shared, compilation, coverage, release, assertJavaVersion)
.settings(
libraryDependencies ++= Seq(
akkaActorTyped, // Needed to create content type
akkaHttpCore,
caffeine,
catsRetry,
circeCore,
Expand All @@ -216,6 +216,7 @@ lazy val kernel = project
log4cats,
pureconfig,
scalaLogging,
munit % Test,
scalaTest % Test
),
addCompilerPlugin(kindProjector),
Expand Down Expand Up @@ -733,29 +734,15 @@ lazy val storage = project
servicePackaging,
coverageMinimumStmtTotal := 75
)
.settings(cargo := {
imsdu marked this conversation as resolved.
Show resolved Hide resolved
import scala.sys.process._

val log = streams.value.log
val cmd = Process(Seq("cargo", "build", "--release"), baseDirectory.value / "permissions-fixer")
if (cmd.! == 0) {
log.success("Cargo build successful.")
(baseDirectory.value / "permissions-fixer" / "target" / "release" / "nexus-fixer") -> "bin/nexus-fixer"
} else {
log.error("Cargo build failed.")
throw new RuntimeException
}
})
.dependsOn(kernel)
.settings(
name := "storage",
moduleName := "storage",
buildInfoKeys := Seq[BuildInfoKey](version),
buildInfoPackage := "ch.epfl.bluebrain.nexus.storage.config",
Docker / packageName := "nexus-storage",
javaSpecificationVersion := "1.8",
imsdu marked this conversation as resolved.
Show resolved Hide resolved
libraryDependencies ++= Seq(
apacheCompress,
apacheIo,
akkaHttp,
akkaHttpCirce,
akkaStream,
Expand All @@ -772,17 +759,15 @@ lazy val storage = project
akkaHttpTestKit % Test,
akkaTestKit % Test,
mockito % Test,
munit % Test,
scalaTest % Test
),
cleanFiles ++= Seq(
baseDirectory.value / "permissions-fixer" / "target" / "**",
baseDirectory.value / "nexus-storage.jar"
),
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-o", "-u", "target/test-reports"),
Test / parallelExecution := false,
Universal / mappings := {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same to put back

(Universal / mappings).value :+ cargo.value
}
Test / parallelExecution := false
)

lazy val tests = project
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ch.epfl.bluebrain.nexus.delta.kernel.http

import akka.http.scaladsl.model.MediaType
import cats.syntax.all._
import pureconfig.ConfigReader
import pureconfig.configurable.genericMapReader
import pureconfig.error.CannotConvert

/**
* Allows to define custom media types for the given extensions
*/
final case class MediaTypeDetectorConfig(extensions: Map[String, MediaType]) {
def find(extension: String): Option[MediaType] = extensions.get(extension)

}

object MediaTypeDetectorConfig {

val Empty = new MediaTypeDetectorConfig(Map.empty)

def apply(values: (String, MediaType)*) = new MediaTypeDetectorConfig(values.toMap)

implicit final val mediaTypeDetectorConfigReader: ConfigReader[MediaTypeDetectorConfig] = {
implicit val mediaTypeConfigReader: ConfigReader[MediaType] =
ConfigReader.fromString(str =>
MediaType
.parse(str)
.leftMap(_ => CannotConvert(str, classOf[MediaType].getSimpleName, s"'$str' is not a valid content type."))
)
implicit val mapReader: ConfigReader[Map[String, MediaType]] = genericMapReader(Right(_))

ConfigReader.fromCursor { cursor =>
for {
obj <- cursor.asObjectCursor
extensionsKey <- obj.atKey("extensions")
extensions <- ConfigReader[Map[String, MediaType]].from(extensionsKey)
} yield MediaTypeDetectorConfig(extensions)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ch.epfl.bluebrain.nexus.delta.kernel.utils

object FileUtils {

/**
* Extracts the extension from the given filename
*/
def extension(filename: String): Option[String] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Allows to get rid of this apache commons io dependency

val lastDotIndex = filename.lastIndexOf('.')
Option.when(lastDotIndex >= 0) {
filename.substring(lastDotIndex + 1)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ch.epfl.bluebrain.nexus.delta.kernel.http

import akka.http.scaladsl.model.ContentTypes
import munit.FunSuite
import pureconfig.ConfigSource

class MediaTypeDetectorConfigSuite extends FunSuite {

private def parseConfig(value: String) =
ConfigSource.string(value).at("media-type-detector").load[MediaTypeDetectorConfig]

test("Parse successfully the config with no defined extension") {
val config = parseConfig(
"""
|media-type-detector {
| extensions {
| }
|}
|""".stripMargin
)

val expected = MediaTypeDetectorConfig.Empty
assertEquals(config, Right(expected))
}

test("Parse successfully the config") {
val config = parseConfig(
"""
|media-type-detector {
| extensions {
| json = application/json
| }
|}
|""".stripMargin
)

val expected = MediaTypeDetectorConfig("json" -> ContentTypes.`application/json`.mediaType)
assertEquals(config, Right(expected))
}

test("Fail to parse the config with an invalid content type") {
val config = parseConfig(
"""
|media-type-detector {
| extensions {
| json = xxx
| }
|}
|""".stripMargin
)

assert(config.isLeft, "Parsing must fail with an invalid content type")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ch.epfl.bluebrain.nexus.delta.kernel.utils

import munit.FunSuite

class FileUtilsSuite extends FunSuite {

test("Detect json extension") {
val obtained = FileUtils.extension("my-file.json")
val expected = Some("json")
assertEquals(obtained, expected)
}

test("Detect zip extension") {
val obtained = FileUtils.extension("my-file.json.zip")
val expected = Some("zip")
assertEquals(obtained, expected)
}

test("Detect no extension") {
val obtained = FileUtils.extension("my-file")
val expected = None
assertEquals(obtained, expected)
}

}
6 changes: 6 additions & 0 deletions delta/plugins/storage/src/main/resources/storage.conf
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ plugins.storage {
files {
# the files event log configuration
event-log = ${app.defaults.event-log}

media-type-detector {
extensions {
#extension = "application/custom"
}
}
}
defaults {
# the name of the default storage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ object Files {
): Files = {
implicit val classicAs: ClassicActorSystem = as.classicSystem
new Files(
FormDataExtractor.apply,
FormDataExtractor(config.mediaTypeDetector),
ScopedEventLog(definition, config.eventLog, xas),
aclCheck,
fetchContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ch.epfl.bluebrain.nexus.delta.plugins.storage.files

import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig
import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig
import pureconfig.ConfigReader
import pureconfig.generic.semiauto.deriveReader
Expand All @@ -10,7 +11,7 @@ import pureconfig.generic.semiauto.deriveReader
* @param eventLog
* configuration of the event log
*/
final case class FilesConfig(eventLog: EventLogConfig)
final case class FilesConfig(eventLog: EventLogConfig, mediaTypeDetector: MediaTypeDetectorConfig)

object FilesConfig {
implicit final val filesConfigReader: ConfigReader[FilesConfig] =
Expand Down
Loading