-
Notifications
You must be signed in to change notification settings - Fork 74
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
Conversation
private val defaultContentType: ContentType.Binary = ContentTypes.`application/octet-stream` | ||
|
||
// Creating an unmarshaller defaulting to `application/octet-stream` as a content type | ||
implicit private val um: FromEntityUnmarshaller[Multipart.FormData] = | ||
MultipartUnmarshallers | ||
.multipartUnmarshaller[Multipart.FormData, Multipart.FormData.BodyPart, Multipart.FormData.BodyPart.Strict]( | ||
mediaRange = `multipart/form-data`, | ||
defaultContentType = defaultContentType, | ||
createBodyPart = (entity, headers) => Multipart.General.BodyPart(entity, headers).toFormDataBodyPart.get, | ||
createStreamed = (_, parts) => Multipart.FormData(parts), | ||
createStrictBodyPart = | ||
(entity, headers) => Multipart.General.BodyPart.Strict(entity, headers).toFormDataBodyPart.get, | ||
createStrict = (_, parts) => Multipart.FormData.Strict(parts) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default from akka is text/plain
, aligning it to application/octet-stream
as in the storage part
A bit verbose but it depends on akka there
@@ -0,0 +1,34 @@ | |||
app { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the way to run the storage in the tests so that it aligns with Delta
More readable than using system properties too
* @param isDir | ||
* flag to decide whether or not the path is a directory | ||
*/ | ||
def apply(path: Path, isDir: Boolean = false): ContentType = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it could be wrapped in a F[_]
/ an IO
but it feels like enough yak shaving and enough complexity for a single PR
customMediaType <- mediaTypeDetector.find(extension) | ||
} yield ContentType(customMediaType, () => HttpCharsets.`UTF-8`) | ||
|
||
bodyDefinedContentType.orElse(detectFromConfig).getOrElse(contentTypeFromAkka) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anything set by the user prevails over automatic detection
/** | ||
* Extracts the extension from the given filename | ||
*/ | ||
def extension(filename: String): Option[String] = { |
There was a problem hiding this comment.
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
build.sbt
Outdated
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 := { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same to put back
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 => | ||
Some(desc -> part.entity) | ||
} | ||
case part => | ||
part.entity.discardBytes().future.as(None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit - this chunk could be its own function
case RejectionError(r) => | ||
WrappedAkkaRejection(r) | ||
case Unmarshaller.NoContentException => | ||
WrappedAkkaRejection(RequestEntityExpectedRejection) | ||
case x: UnsupportedContentTypeException => | ||
WrappedAkkaRejection(UnsupportedRequestContentTypeRejection(x.supported, x.actualContentType)) | ||
case x: IllegalArgumentException => | ||
WrappedAkkaRejection(ValidationRejection(Option(x.getMessage).getOrElse(""), Some(x))) | ||
case x: ExceptionWithErrorInfo => | ||
WrappedAkkaRejection(MalformedRequestContentRejection(x.info.format(withDetail = false), x)) | ||
case x => | ||
WrappedAkkaRejection(MalformedRequestContentRejection(Option(x.getMessage).getOrElse(""), x)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment - could have an error handling function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is a lot of code integrating the akka that is going over my head, but this seems fine in principle
...e/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FormDataExtractor.scala
Show resolved
Hide resolved
response.status shouldEqual StatusCodes.Created | ||
} | ||
storageId2 = s"${storageId}2" | ||
_ <- deltaClient.get[Json](s"/storages/$fullId/nxv:$storageId2", Coyote) { (json, response) => | ||
val expected = storageResponse(fullId, storageId2, s"$storageName/read", s"$storageName/write") | ||
_ <- deltaClient.get[Json](s"/storages/$projectRef/nxv:$storageId2", Coyote) { (json, response) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this change
test("Detect overridden content type") { | ||
val customMediaType = MediaTypes.`application/vnd.api+json` | ||
val detector = new ContentTypeDetector(MediaTypeDetectorConfig("json" -> MediaTypes.`application/vnd.api+json`)) | ||
val expected = ContentType(customMediaType, () => `UTF-8`) | ||
assertEquals(detector(jsonPath, isDir = false), expected) | ||
} | ||
|
||
test("Detect overridden content type") { | ||
val customMediaType = MediaTypes.`application/vnd.api+json` | ||
val detector = new ContentTypeDetector(MediaTypeDetectorConfig("json" -> MediaTypes.`application/vnd.api+json`)) | ||
val expected = ContentType(customMediaType, () => `UTF-8`) | ||
assertEquals(detector(jsonPath, isDir = false), expected) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate?
Fixes #4266