<commit_before>package lila.memo

import akka.stream.scaladsl.{ FileIO, Source }
import akka.util.ByteString
import org.joda.time.DateTime
import play.api.libs.ws.StandaloneWSClient
import play.api.mvc.MultipartFormData
import reactivemongo.api.bson.Macros
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext

import lila.common.config
import lila.db.dsl._
import com.github.blemale.scaffeine.LoadingCache

case class PicfitImage(
    _id: PicfitImage.Id,
    user: String,
    // reverse reference like blog:id, streamer:id, coach:id, ...
    // unique: a new image will delete the previous ones with same rel
    rel: String,
    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, config: PicfitConfig)(implicit
    ec: ExecutionContext
) {

  import PicfitApi._
  private val uploadMaxBytes = uploadMaxMb * 1024 * 1024

  def upload(rel: 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 10),
        user = userId,
        rel = rel,
        name = sanitizeName(uploaded.filename),
        contentType = uploaded.contentType,
        size = uploaded.fileSize.toInt,
        createdAt = DateTime.now
      )
      picfitServer.store(image, uploaded) >>
        deletePrevious(image) >>
        coll.insert.one(image) inject image
    }

  private def deletePrevious(image: PicfitImage): Funit =
    coll
      .findAndRemove($doc("rel" -> image.rel, "_id" $ne image.id))
      .flatMap { _.result[PicfitImage] ?? picfitServer.delete }
      .void

  private def sanitizeName(name: String) = {
    // the char `^` breaks play, even URL encoded
    java.net.URLEncoder.encode(name, "UTF-8").replaceIf('%', "")
  }

  private object picfitServer {

    def store(image: PicfitImage, from: Uploaded): Funit = from.contentType collect {
      case "image/png"  => "png"
      case "image/jpeg" => "jpg"
    } match {
      case None => fufail(s"Invalid file type: ${from.contentType | "unknown"}")
      case Some(extension) => {
        type Part = MultipartFormData.FilePart[Source[ByteString, _]]
        import WSBodyWritables._
        val part: Part = MultipartFormData.FilePart(
          key = "data",
          filename = s"${image.id.value}.$extension",
          contentType = from.contentType,
          ref = FileIO.fromPath(from.ref.path),
          fileSize = from.fileSize
        )
        val source: Source[Part, _] = Source(part :: List())
        ws.url(s"${config.endpointPost}/upload")
          .post(source)
          .flatMap {
            case res if res.status != 200 => fufail(s"${res.statusText} ${res.body take 200}")
            case _ =>
              lila.mon.picfit.uploadSize(image.user).record(image.size)
              funit
          }
          .monSuccess(_.picfit.uploadTime(image.user))
      }
    }

    def delete(image: PicfitImage): Funit =
      ws.url(s"${config.endpointPost}/${image.id}").delete().flatMap {
        case res if res.status != 200 =>
          logger
            .branch("picfit")
            .error(s"deleteFromPicfit ${image.id} ${res.statusText} ${res.body take 200}")<commit_msg>store picfit image extension in the ID<commit_after>package lila.memo

import akka.stream.scaladsl.{ FileIO, Source }
import akka.util.ByteString
import org.joda.time.DateTime
import play.api.libs.ws.StandaloneWSClient
import play.api.mvc.MultipartFormData
import reactivemongo.api.bson.Macros
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext

import lila.common.config
import lila.db.dsl._
import com.github.blemale.scaffeine.LoadingCache

case class PicfitImage(
    _id: PicfitImage.Id,
    user: String,
    // reverse reference like blog:id, streamer:id, coach:id, ...
    // unique: a new image will delete the previous ones with same rel
    rel: String,
    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, config: PicfitConfig)(implicit
    ec: ExecutionContext
) {

  import PicfitApi._
  private val uploadMaxBytes = uploadMaxMb * 1024 * 1024

  def upload(rel: String, uploaded: Uploaded, userId: String): Fu[PicfitImage] =
    if (uploaded.fileSize > uploadMaxBytes)
      fufail(s"File size must not exceed ${uploadMaxMb}MB.")
    else
      uploaded.contentType collect {
        case "image/png"  => "png"
        case "image/jpeg" => "jpg"
      } match {
        case None => fufail(s"Invalid file type: ${uploaded.contentType | "unknown"}")
        case Some(extension) => {
          val image = PicfitImage(
            _id = PicfitImage.Id(s"${lila.common.ThreadLocalRandom nextString 10}.$extension"),
            user = userId,
            rel = rel,
            name = sanitizeName(uploaded.filename),
            contentType = uploaded.contentType,
            size = uploaded.fileSize.toInt,
            createdAt = DateTime.now
          )
          picfitServer.store(image, uploaded) >>
            deletePrevious(image) >>
            coll.insert.one(image) inject image
        }
      }

  private def deletePrevious(image: PicfitImage): Funit =
    coll
      .findAndRemove($doc("rel" -> image.rel, "_id" $ne image.id))
      .flatMap { _.result[PicfitImage] ?? picfitServer.delete }
      .void

  private def sanitizeName(name: String) = {
    // the char `^` breaks play, even URL encoded
    java.net.URLEncoder.encode(name, "UTF-8").replaceIf('%', "")
  }

  private object picfitServer {

    def store(image: PicfitImage, from: Uploaded): Funit = {
      type Part = MultipartFormData.FilePart[Source[ByteString, _]]
      import WSBodyWritables._
      val part: Part = MultipartFormData.FilePart(
        key = "data",
        filename = image.id.value,
        contentType = from.contentType,
        ref = FileIO.fromPath(from.ref.path),
        fileSize = from.fileSize
      )
      val source: Source[Part, _] = Source(part :: List())
      ws.url(s"${config.endpointPost}/upload")
        .post(source)
        .flatMap {
          case res if res.status != 200 => fufail(s"${res.statusText} ${res.body take 200}")
          case _ =>
            lila.mon.picfit.uploadSize(image.user).record(image.size)
            funit
        }
        .monSuccess(_.picfit.uploadTime(image.user))
    }

    def delete(image: PicfitImage): Funit =
      ws.url(s"${config.endpointPost}/${image.id}").delete().flatMap {
        case res if res.status != 200 =>
          logger
            .branch("picfit")
            .error(s"deleteFromPicfit ${image.id} ${res.statusText} ${res.body take 200}")
{'fraction_non_alphanumeric': 0.1183591123066577, 'fraction_numerical': 0.006186953597848016, 'mean_word_length': 2.7958141909137315, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2251180', 'n_tokens_mistral': 2368, 'n_tokens_neox': 2281, 'n_words': 523}