Scala Play Framework image upload with Angular ng-

2019-09-15 10:37发布

问题:

I am using Angular ng-file-upload (https://github.com/danialfarid/ng-file-upload) on the frontend to manage the file upload process.

Unfortunately, form contains a complex object with multiple files. Using the MultipartFormData (https://www.playframework.com/documentation/2.5.x/ScalaBodyParsers) on the server side I have successfully decomposed the uploaded content and can read it from the request.body.

Now, to my surprise, I do not have a simple Json Objects but rather a strangely formed datatype, described on the ng-file-upload website as:

(...) server implementations expecting nested data object keys in .key or [key] format. Example: data: {rec: {name: 'N', pic: file}} sent as: rec[name] -> N, rec[pic] -> file
data: {rec: {name: 'N', pic: file}, objectKey: '.k'} sent as: rec.name -> N, rec.pic -> file

So far I have managed to bring all the data to a common MultipartFormData.Part type, using the DataPart and FilePart like this:

 val opts = body.dataParts.map {
   case (key, values) => DataPart(key, values.head)
 }

 val parts = opts ++ body.files

So I am now left with a quite unfortunate Iterable[Part]:

0 = {MultipartFormData$DataPart@86271} "DataPart(arabic[active],false)"
1 = {MultipartFormData$DataPart@86273} "DataPart(english[active],true)"
2 = {MultipartFormData$DataPart@86277} "DataPart(english[url],2132132132)"
...
7 = {MultipartFormData$FilePart@76473} "FilePart(english[image],fb_icon_325x325.png,Some(image/png),TemporaryFile(/tmp/playtemp5909927824995768544/multipartBody8348573128070542611asTemporaryFile))"

Each object name contains the key of it's Json structure and its according value. Now instead of key[level1][level2] I would like to parse it to objects, in my case:

case class PcBanner(english: PcBanners, arabic: PcBanners, kurdish: PcBanners)
case class PcBanners(active: Boolean, url: Option[String], image: Option[String])`

I hope you got the idea.

The question

I know I could try to parse the name strings trying to fit it to objects, but I believe I made a mistake someway in the middle. Is there a way to parse this structure into the objects, using field names as a reference? Any build in Play functions or alike?

Thanks for help!

回答1:

As I stated in the title my case was to send images. As you would expect, I am also presenting a preview and the files currently saved in the database.

Considering all pros and cons I have decided to send all the data in JSON format, both ways. Meaning that the images are encoded and sent along in JSON structure.

Despite the fact that above solution looks very convenient it actually creates new problems during the implementation.

  1. You will quickly exceed the server's POST request size limit. For Play server the default 100kB is possible to be extended, but...
  2. I have soon run into some data malformations as the image saved as huge String of bytes probably had some sending/parsing errors.

Not going deeper into this faulty solution I have used the @danial advice:

No have the file sent separately like this
{file: file, otherData: JSON.stringify(myData)}

My solution

If anyone would like to use similar approach to mine I present my answer. On the front-end side I have decided used ng-file-upload library. Binding it to HTML component with ngf-select with ngf-drop which enables the component:

<div ngf-drop ngf-select
     ng-model="image"
     ngf-accept="'image/*'"
     ngf-resize="{width: {{width}}, height: {{height}}, quality: 1.0, restoreExif: false}">

    <img ng-show="!!image && !!image.$ngfName" ngf-src="image">

    <img ng-show="(!image || !image.$ngfName)" ng-src="{{ imageUrl }}">
</div>

Inside the upload tag I put the image preview. This works flawlessly. If the image is not selected I use the image saved in the db.

The data and images do not share the model anymore. The upload function looks as follow:

return Upload.upload({
    url: url,
    data: {file: images, data: angular.toJson(data)}
}).then(function (resp) {
    console.log(resp);
}, function (error) {
    console.log(error);
});

Putting together all the above gave me the output data object:

{  
   "english":{  
      "active":true,
      "url":"http://google.com"
   },
   "arabic":{  
      "active":true,
      "url":"http://google.com"
   },
   "kurdish":{  
      "active":true,
      "url":"http://google.com"
   }
}

On the server side the JSON matches the prepared case class and is parsed with build-in Jackson parser, allowing for easy object manipulation. The image has to be manually selected:

val json = r.body.dataParts("data")
val jsValue = Json.parse(json.head)
val result = jsValue.validate(LocalizedBanner.dataModelFormat) // parse JSON

Extracting the files from body can be done with build in function .file:

val key = s"file[${lang.name}][${imageType.name}]"
body.file(key).map(mp => (mp.ref.file, imageType))

Enjoy!