3

I want to have a service which receives an item object, the object contains; name, description, price and picture.

  1. the other attributes are strings which easily can be sent as Json object but for including picture what is the best solution?
  2. if multipart formdata is the best solution how it is handled in Lagom?
Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
Amir-Mousavi
  • 4,273
  • 12
  • 70
  • 123

1 Answers1

1

You may want to check the file upload example in the lagom-recipes repository on GitHub.

Basically, the idea is to create an additional Play router. After that, we have to tell Lagom to use it as noted in the reference documentation (this feature is available since 1.5.0). Here is how the router might look like:

class FileUploadRouter(action: DefaultActionBuilder,
                       parser: PlayBodyParsers,
                       implicit val exCtx: ExecutionContext) {
  private def fileHandler: FilePartHandler[File] = {
    case FileInfo(partName, filename, contentType, _) =>
      val tempFile = {
        val f = new java.io.File("./target/file-upload-data/uploads", UUID.randomUUID().toString).getAbsoluteFile
        f.getParentFile.mkdirs()
        f
      }
      val sink: Sink[ByteString, Future[IOResult]] = FileIO.toPath(tempFile.toPath)
      val acc: Accumulator[ByteString, IOResult] = Accumulator(sink)
      acc.map {
        case akka.stream.IOResult(_, _) =>
          FilePart(partName, filename, contentType, tempFile)
      }

  }
  val router = Router.from {
    case POST(p"/api/files") =>
      action(parser.multipartFormData(fileHandler)) { request =>
        val files = request.body.files.map(_.ref.getAbsolutePath)
        Results.Ok(files.mkString("Uploaded[", ", ", "]"))
      }
  }
}

And then, we simply tell Lagom to use it

  override lazy val lagomServer =
    serverFor[FileUploadService](wire[FileUploadServiceImpl])
      .additionalRouter(wire[FileUploadRouter].router)

Alternatively, we can make use of the PlayServiceCall class. Here is a simple sketch on how to do that provided by James Roper from the Lightbend team:

// The type of the service call is NotUsed because we are handling it out of band
def myServiceCall: ServiceCall[NotUsed, Result] = PlayServiceCall { wrapCall =>
  // Create a Play action to handle the request
  EssentialAction { requestHeader =>

    // Now we create the sink for where we want to stream the request to - eg it could
    // go to a file, a database, some other service. The way Play gives you a request
    // body is that you need to return a sink from EssentialAction, and when it gets
    // that sink, it stream the request body into that sink.
    val sink: Sink[ByteString, Future[Done]] = ...

    // Play wraps sinks in an abstraction called accumulator, which makes it easy to
    // work with the result of handling the sink. An accumulator is like a future, but
    // but rather than just being a value that will be available in future, it is a
    // value that will be available once you have passed a stream of data into it.
    // We wrap the sink in an accumulator here.
    val accumulator: Accumulator[ByteString, Done] = Accumulator.forSink(sink)

    // Now we have an accumulator, but we need the accumulator to, when it's done,
    // produce an HTTP response.  Right now, it's just producing akka.Done (or whatever
    // your sink materialized to).  So we flatMap it, to handle the result.
    accumulator.flatMap { done =>

      // At this point we create the ServiceCall, the reason we do that here is it means
      // we can access the result of the accumulator (in this example, it's just Done so
      // not very interesting, but it could be something else).
      val wrappedAction = wrapCall(ServiceCall { notUsed =>

        // Here is where we can do any of the actual business logic, and generate the
        // result that can be returned to Lagom to be serialized like normal

        ...
      })

      // Now we invoke the wrapped action, and run it with no body (since we've already
      // handled the request body with our sink/accumulator.
      wrappedAction(request).run()
    }
  }
}

Generally speaking, it probably isn't a good idea to use Lagom for that purpose. As noted on the GitHub issue on PlayServiceCall documentation:

Many use cases where we fallback to PlayServiceCall are related to presentation or HTTP-specific use (I18N, file upload, ...) which indicate: coupling of the lagom service to the presentation layer or coupling of the lagom service to the transport.

Quoting James Roper again (a few years back):

So currently, multipart/form-data is not supported in Lagom, at least not out of the box. You can drop down to a lower level Play API to handle it, but perhaps it would be better to handle it in a web gateway, where any files handled are uploaded directly to a storage service such as S3, and then a Lagom service might store the meta data associated with it.

You can also check the discussion here, which provides some more insight.

arnaudoff
  • 686
  • 8
  • 20
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/low-quality-posts/24011220) – Krzysztof Atłasik Sep 10 '19 at 11:06
  • @KrzysztofAtłasik, sorry, didn't know that. Tried to point him in the right direction with more explanation now. – arnaudoff Sep 10 '19 at 12:00
  • Please just add an example using sources you're provided and you will surely get the accepted answer. +1 from me – Krzysztof Atłasik Sep 10 '19 at 12:02
  • 1
    Done, I'm just hoping to help some more people that have the same problem as I had, whether it's accepted or not doesn't matter. Cheers. – arnaudoff Sep 10 '19 at 12:17
  • Sure, I just wanted to explain why some you had downvotes. Great work anyway! – Krzysztof Atłasik Sep 10 '19 at 12:23