Question

I am using Play Framework and ReactiveMongo DB in order to store an uploaded file in GridFS. Here is the code:

  def saveAttachment(id: String) = Action.async(gridFSBodyParser(gridFS)) { request =>
  // here is the future file!
    val futureFile = request.body.files.head.ref
    // when the upload is complete, we add the article id to the file entry (in order to find the attachments of the article)
    val futureUpdate = for {
      file <- futureFile
      // here, the file is completely uploaded, so it is time to update the user
      updateResult <- {
        gridFS.files.update(
          BSONDocument("_id" -> file.id),
          BSONDocument("$set" -> BSONDocument("user" -> BSONObjectID(id)),
            "$set" -> BSONDocument("size" -> "original")))
      }
    } yield updateResult

    futureUpdate.map {
      case _ => Ok("xx")
    }.recover {
      case e => InternalServerError(e.getMessage())
    }
  }

How can I add another file with the property "size" -> "thumb" with the same uploaded image but resized for a small thumb? I have here 2 problems:

  • How do I store 2 files in GridFS on a single upload?
  • How do I resize the image before storing it?

Thanks for your answer. It is a good direction. I was looking for a solution with GridFS. Here is what I ended up with:

  def saveAttachment(id: String) = Action.async(gridFSBodyParser(gridFS)) { request =>
  // here is the future file!
    val futureFile = request.body.files.head.ref
    // when the upload is complete, we add the article id to the file entry (in order to find the attachments of the article)
    val futureUpdate = for {
      file <- futureFile
      // here, the file is completely uploaded, so it is time to update the user
      updateResult <- {
        gridFS.files.update(
          BSONDocument("_id" -> file.id),
          BSONDocument("$set" -> BSONDocument("metadata" ->
                        BSONDocument("user" -> BSONObjectID(id),
                                      "size" -> "original"))))


        val iterator = gridFS.enumerate(file).run(Iteratee.consume[Array[Byte]]())
        iterator.flatMap {
          bytes => {
            // Create resized image
            val enumerator: Enumerator[Array[Byte]] = Enumerator.outputStream(
              out => {
                Image(bytes).bound(120, 120).writer(Format.JPEG).withCompression(90).write(out)
              }
            )

            val data = DefaultFileToSave(
              filename = file.filename,
              contentType = file.contentType,
              uploadDate = Some(DateTime.now().getMillis),
              metadata = file.metadata ++ BSONDocument(
                "user" -> BSONObjectID(id),
                "size" -> "thumb"
              )
            )

            Logger.warn(s"Saving resized image: [id=$id, metadata=${data.metadata}}]")
            gridFS.save(enumerator, data).map {
              image => Some(image)
            }
          }
        }
      }
    } yield updateResult

    futureUpdate.map {
      case _ => Ok("xx")
    }.recover {
      case e => InternalServerError(e.getMessage())
    }
  }
Was it helpful?

Solution

The solution is quite straightforward, I use scrimage for image processing, and here's my controller:

// imports omitted

def upload = Authenticated.async(parse.multipartFormData) { request =>

  // some plumbing omitted

  request.body.file("photo") match {
    // validations omitted
    case Some(photo) =>
      val fileToSave = DefaultFileToSave(photo.filename, photo.contentType)
      val resizedFile = Image(photo.ref.file).fitToWidth(120).write
      val enumerator = Enumerator(resizedFile)
      gfs.save(enumerator, fileToSave) map {
        case file =>
          val id = file.id.asInstanceOf[BSONObjectID].stringify
          Ok(obj("files" -> Seq(obj(
            "id" -> id,
            "name" -> file.filename,
            "size" -> file.length,
            "url" -> routes.Photos.photo(id).url,
            "thumbnailUrl" -> routes.Photos.photo(id).url,
            "deleteUrl" -> "",
            "deleteType" -> "DELETE"
          ))))
      } recover {
        case e =>
          Logger.error(e.toString)
          InternalServerError("upload failed")
      }
    }
  }
}

Please note the resizing part.

The first problem you can solve for example by simply copying underlying file and making a second save call with a resized copy. I don't have a complete solution in code at hand, so feel free to ask for details if you'll need further help.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top