From 3fe1736fa583a433e60e523f82fbc5990bb893d8 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 30 Aug 2021 12:32:06 +0200 Subject: [PATCH] user blog & picfit WIP --- app/controllers/Ublog.scala | 42 ++++++++++ build.sbt | 2 +- conf/base.conf | 4 + conf/routes | 1 + modules/memo/src/main/Env.scala | 9 ++- modules/memo/src/main/Picfit.scala | 80 +++++++++++++++++++ modules/ublog/src/main/Env.scala | 4 +- modules/ublog/src/main/UblogApi.scala | 8 +- .../ublog/src/main/UblogBsonHandlers.scala | 1 + modules/ublog/src/main/UblogForm.scala | 1 + modules/ublog/src/main/UblogPost.scala | 2 + 11 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 modules/memo/src/main/Picfit.scala diff --git a/app/controllers/Ublog.scala b/app/controllers/Ublog.scala index 473ef5ef27..734a46782a 100644 --- a/app/controllers/Ublog.scala +++ b/app/controllers/Ublog.scala @@ -79,4 +79,46 @@ final class Ublog(env: Env) extends LilaController(env) { } } } + + // private val handleFilePartAsFile: play.core.parsers.Multipart.FilePartHandler[java.io.File] = { + // case Multipart.FileInfo(partName, filename, contentType, dispositionType) => + // val perms = java.util.EnumSet.of(OWNER_READ, OWNER_WRITE) + // val attr = PosixFilePermissions.asFileAttribute(perms) + // val path = JFiles.createTempFile("multipartBody", "tempFile", attr) + // val file = path.toFile + // val fileSink = FileIO.toPath(path) + // val accumulator = Accumulator(fileSink) + // accumulator.map { case IOResult(count, status) => + // FilePart(partName, filename, contentType, file, count, dispositionType) + // }(ec) + // } + import play.api.mvc.BodyParser + import akka.stream.scaladsl.Source + import akka.util.ByteString + import play.api.libs.streams.Accumulator + def verbatimBodyParser: BodyParser[Source[ByteString, _]] = BodyParser { _ => + // Return the source directly. We need to return + // an Accumulator[Either[Result, T]], so if we were + // handling any errors we could map to something like + // a Left(BadRequest("error")). Since we're not + // we just wrap the source in a Right(...) + Accumulator.source[ByteString].map(Right.apply) + } + + def image(unusedUsername: String, id: String) = + AuthBody(parse.multipartFormData(handleFilePartAsFile)) { implicit ctx => me => + env.ublog.api.findByAuthor(UblogPost.Id(id), me) flatMap { + _ ?? { post => + ctx.body.body.file("image") match { + case FilePart(key, filename, contentType, file, fileSize, dispositionType) => + case Some(image) => + env.ublog.api.uploadImage(post, image) recover { case e: Exception => + BadRequest(html.ublog.form.edit(me, post, env.ublog.form.edit(post))) + .flashFailure(e.getMessage) + } inject Redirect(routes.Streamer.edit) + case None => fuccess(Redirect(routes.Streamer.edit)) + } + } + } + } } diff --git a/build.sbt b/build.sbt index 4073d2b9a0..24be1add35 100644 --- a/build.sbt +++ b/build.sbt @@ -152,7 +152,7 @@ lazy val db = smallModule("db", lazy val memo = smallModule("memo", Seq(common, db), - Seq(scaffeine, autoconfig, scalatest, akka.testkit) ++ reactivemongo.bundle ++ macwire.bundle + Seq(scaffeine, autoconfig, scalatest, akka.testkit) ++ reactivemongo.bundle ++ macwire.bundle ++ playWs.bundle ) lazy val search = smallModule("search", diff --git a/conf/base.conf b/conf/base.conf index da26d45412..721517386d 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -385,6 +385,10 @@ memo { cache = cache config = flag } + picfit { + collection = picfit_image + endpoint = "127.0.0.1:3001" + } } redis { uri = "redis://127.0.0.1" diff --git a/conf/routes b/conf/routes index 4707d4d082..5023315387 100644 --- a/conf/routes +++ b/conf/routes @@ -64,6 +64,7 @@ GET /@/:username/blog/new controllers.Ublog.form(username: String) POST /@/:username/blog/new controllers.Ublog.create(username: String) GET /@/:username/blog/$id<\w{8}>/edit controllers.Ublog.edit(username: String, id: String) POST /@/:username/blog/$id<\w{8}>/edit controllers.Ublog.update(username: String, id: String) +POST /@/:username/blog/$id<\w{8}>/image controllers.Ublog.image(username: String, id: String) GET /@/:username/blog/:slug/:id controllers.Ublog.post(username: String, slug: String, id: String) # User diff --git a/modules/memo/src/main/Env.scala b/modules/memo/src/main/Env.scala index 96468344c3..a399ab09b2 100644 --- a/modules/memo/src/main/Env.scala +++ b/modules/memo/src/main/Env.scala @@ -8,14 +8,17 @@ import lila.common.config._ final class MemoConfig( @ConfigName("collection.cache") val cacheColl: CollName, - @ConfigName("collection.config") val configColl: CollName + @ConfigName("collection.config") val configColl: CollName, + @ConfigName("picfit.collection") val picfitColl: CollName, + @ConfigName("picfit.endpoint") val picfitEndpoint: String ) @Module final class Env( appConfig: Configuration, mode: play.api.Mode, - db: lila.db.Db + db: lila.db.Db, + ws: play.api.libs.ws.StandaloneWSClient )(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem) { private val config = appConfig.get[MemoConfig]("memo")(AutoConfig.loader) @@ -29,4 +32,6 @@ final class Env( lazy val mongoCacheApi = wire[MongoCache.Api] lazy val mongoRateLimitApi = wire[MongoRateLimitApi] + + lazy val picfitApi = new PicfitApi(db(config.picfitColl), ws, endpoint = config.picfitEndpoint) } diff --git a/modules/memo/src/main/Picfit.scala b/modules/memo/src/main/Picfit.scala new file mode 100644 index 0000000000..6091b71bb4 --- /dev/null +++ b/modules/memo/src/main/Picfit.scala @@ -0,0 +1,80 @@ +package lila.memo + +import java.nio.file.Path +import org.joda.time.DateTime +import reactivemongo.api.bson.Macros +import scala.concurrent.ExecutionContext +import play.api.libs.ws.StandaloneWSClient + +import lila.db.dsl._ + +case class PicfitImage( + _id: PicfitImage.Id, + user: String, + scope: String, // like blog, streamer, coach, ... + name: String, + contentType: Option[String], + size: Int, // in bytes + createdAt: DateTime +) { + + def id = _id +} + +object PicfitImage { + + case class Id(value: String) extends AnyVal with StringValue + + implicit val imageIdBSONHandler = stringAnyValHandler[PicfitImage.Id](_.value, PicfitImage.Id.apply) + implicit val imageBSONHandler = Macros.handler[PicfitImage] +} + +final class PicfitApi(coll: Coll, ws: StandaloneWSClient, endpoint: String)(implicit ec: ExecutionContext) { + + import PicfitApi._ + private val uploadMaxBytes = uploadMaxMb * 1024 * 1024 + + def upload(scope: String, uploaded: Uploaded, userId: String): Fu[PicfitImage] = + if (uploaded.fileSize > uploadMaxBytes) + fufail(s"File size must not exceed ${uploadMaxMb}MB.") + else { + val image = PicfitImage( + _id = PicfitImage.Id(lila.common.ThreadLocalRandom nextString 8), + user = userId, + scope = scope, + name = sanitizeName(uploaded.filename), + contentType = uploaded.contentType, + size = uploaded.fileSize.toInt, + createdAt = DateTime.now + ) + // ws.url(s"$endpoint/upload") + // .post( + // Source( + // FilePart( + // "File", + // fileName, + // Option("application/pdf"), + // FileIO.fromPath(Paths.get(pathToFile)) + // ) :: List() + // ) + // ) + // .post(body(source)) flatMap { + // case res if res.status != 200 => + // fufail(res.statusText) + // case res => funit + // } >> + coll.insert.one(image) inject image + } + + private def sanitizeName(name: String) = { + // the char `^` breaks play, even URL encoded + java.net.URLEncoder.encode(name, "UTF-8").replaceIf('%', "") + } +} + +object PicfitApi { + + val uploadMaxMb = 4 + + type Uploaded = play.api.mvc.MultipartFormData.FilePart[play.api.libs.Files.TemporaryFile] +} diff --git a/modules/ublog/src/main/Env.scala b/modules/ublog/src/main/Env.scala index dbca067aaa..96f01e7460 100644 --- a/modules/ublog/src/main/Env.scala +++ b/modules/ublog/src/main/Env.scala @@ -7,10 +7,12 @@ import lila.common.config._ @Module final class Env( db: lila.db.Db, - userRepo: lila.user.UserRepo + userRepo: lila.user.UserRepo, + picfit: lila.memo.PicfitApi )(implicit ec: scala.concurrent.ExecutionContext ) { + private val postColl = db(CollName("ublog_post")) val api = wire[UblogApi] diff --git a/modules/ublog/src/main/UblogApi.scala b/modules/ublog/src/main/UblogApi.scala index 8d80c3021e..1876064a47 100644 --- a/modules/ublog/src/main/UblogApi.scala +++ b/modules/ublog/src/main/UblogApi.scala @@ -7,9 +7,10 @@ import lila.common.config.MaxPerPage import lila.common.paginator.Paginator import lila.db.dsl._ import lila.db.paginator.Adapter +import lila.memo.PicfitApi import lila.user.User -final class UblogApi(coll: Coll)(implicit ec: ExecutionContext) { +final class UblogApi(coll: Coll, picfitApi: PicfitApi)(implicit ec: ExecutionContext) { import UblogBsonHandlers._ @@ -34,6 +35,11 @@ final class UblogApi(coll: Coll)(implicit ec: ExecutionContext) { def draftByUser(user: User, page: Int): Fu[Paginator[UblogPost]] = paginatorByUser(user, false, page) + def uploadImage(post: UblogPost, picture: PicfitApi.Uploaded) = + picfitApi.upload("ublog", picture, userId = post.user).flatMap { image => + coll.update.one($id(post.id), $set("image" -> image.id)).void + } + private def paginatorByUser(user: User, live: Boolean, page: Int): Fu[Paginator[UblogPost]] = Paginator( adapter = new Adapter[UblogPost]( diff --git a/modules/ublog/src/main/UblogBsonHandlers.scala b/modules/ublog/src/main/UblogBsonHandlers.scala index 1f0198218f..e79820d2da 100644 --- a/modules/ublog/src/main/UblogBsonHandlers.scala +++ b/modules/ublog/src/main/UblogBsonHandlers.scala @@ -5,6 +5,7 @@ import reactivemongo.api.bson._ private[ublog] object UblogBsonHandlers { + import lila.memo.PicfitImage.imageIdBSONHandler implicit val postIdBSONHandler = stringAnyValHandler[UblogPost.Id](_.value, UblogPost.Id.apply) implicit val postBSONHandler = Macros.handler[UblogPost] } diff --git a/modules/ublog/src/main/UblogForm.scala b/modules/ublog/src/main/UblogForm.scala index 8db788bd14..162b757127 100644 --- a/modules/ublog/src/main/UblogForm.scala +++ b/modules/ublog/src/main/UblogForm.scala @@ -38,6 +38,7 @@ object UblogForm { title = title, intro = intro, markdown = markdown, + image = none, live = false, createdAt = now, updatedAt = now, diff --git a/modules/ublog/src/main/UblogPost.scala b/modules/ublog/src/main/UblogPost.scala index 02c4038170..3b360541ea 100644 --- a/modules/ublog/src/main/UblogPost.scala +++ b/modules/ublog/src/main/UblogPost.scala @@ -2,6 +2,7 @@ package lila.ublog import org.joda.time.DateTime +import lila.memo.PicfitImage import lila.user.User case class UblogPost( @@ -10,6 +11,7 @@ case class UblogPost( title: String, intro: String, markdown: String, + image: Option[PicfitImage.Id], live: Boolean, createdAt: DateTime, updatedAt: DateTime,