🥰

Scala の http4s で静的ファイルを配信する(Basic 認証もあるヨ)

2023/04/19に公開

最小構成(Scala CLI)

tree
.
├── public
│   └── index.html
└── server.scala
index.html
<html>
    <body>
        <p>
            Hello, World!
        </p>
    </body>
</html>
server.scala
//> using scala "3.2.2"
//> using dep "org.typelevel::cats-effect::3.5.0-RC2"
//> using dep "org.http4s::http4s-core::1.0.0-M39"
//> using dep "org.http4s::http4s-ember-server::1.0.0-M39"

import cats.effect.*
import cats.effect.std.Env
import com.comcast.ip4s.*
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.staticcontent.*

object Docs extends IOApp:
  override def run(args:List[String]) : IO[ExitCode] =
    
    val app = (path:String) => EmberServerBuilder
      .default[IO]
      .withHost(ipv4"0.0.0.0")
      .withPort(port"8080")
      // FileService.Config(path) で静的ファイルを配信するディレクトリを指定する.
      .withHttpApp(fileService[IO](FileService.Config(path)).orNotFound)
      .build
      .use(_ => IO.never)
    for
      path  <- Env[IO].get("DOCS_BASE_DIR")
      handle <- app(path.getOrElse("public")).start
      _ <- handle.joinWithNever
    yield ExitCode.Success
DOCS_BASE_DIR=public scala-cli server.scala

http://localhost:8080 にアクセスする.

Scala Native に対応する

Scala Native を利用することでJVM に依存しないスタートアップが高速なバイナリを作ることができます.

コンテナに入れる場合は JVM がいらないのでイメージサイズを約 1/5 に減らすことができます.

// Scala Native の場合は epollcat の依存を追加
+ //> using dep "com.armanbilge::epollcat::0.1.4"

+ import epollcat.EpollApp
- object Docs extends IOApp:
+ object Docs extends EpollApp:
scala-cli package server.scala --native -o server
./server

ld -lcrypto not found などのエラーが出た場合は libssl-dev をインスコしましょう.

apt install -y libssl-dev

Basic 認証をつける

//> using scala "3.2.2"
//> using lib "org.typelevel::cats-effect::3.5.0-RC2"
//> using lib "org.http4s::http4s-core::1.0.0-M39"
//> using lib "org.http4s::http4s-ember-server::1.0.0-M39"
// Scala Native の場合は epollcat の依存を追加
/* //> using lib "com.armanbilge::epollcat::0.1.4" */

import cats.effect.*
import cats.effect.std.Env
import com.comcast.ip4s.*
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.staticcontent.*
// Scala Native の場合は以下の import を追加
// import epollcat.EpollApp

// 以下の import を追加します.
import org.http4s.AuthedRoutes
import org.http4s.ContextRequest
import org.http4s.Response
import org.http4s.Status
import org.http4s.server.middleware.authentication.BasicAuth

// Scala Native の場合は、EpollApp を利用する.
// object Docs extends EpollApp:
object Docs extends IOApp:
  override def run(args: List[String]): IO[ExitCode] =
    val createFileService = (path: String) =>
      fileService[IO](FileService.Config(path))
    // ここで BasicAuth ミドルウェアを設定します. 
    // 認証情報から構造体を復元したりしないので BasicAuth[IO, Unit] で 
    // Unit を指定します.
    // JWT を使うケースなど認証時に User などのデータが得られる場合は
    // 認証系 middleware でその型とデシリアイズの実装を書きます.
    // ここで得られる値は下の `case ContextRequest(_, req) の
    // 一つ目のフィールドに入っています.
    
    def naiveBasicAuthMiddleware(routes: org.http4s.HttpRoutes[IO]) =
      val middleware = BasicAuth[IO, Unit](
        "<realm>",
        cred =>
          IO(
            Option.when(
              cred.username == ??? && cred.password == ???
            )(())
          )
      )
      middleware(AuthedRoutes.of[Unit, IO] { case ContextRequest(_, req) =>
        routes.run(req).value.map {
          case None        => Response(Status.BadRequest)
          case Some(value) => value
        }
      })

    val app = (service: org.http4s.HttpRoutes[IO]) =>
      EmberServerBuilder
        .default[IO]
        .withHost(ipv4"0.0.0.0")
        .withPort(port"8080")
        .withHttpApp(service.orNotFound)
        .build
        .use(_ => IO.never)
    for
      path <- Env[IO].get("DOCS_BASE_DIR")
      service = createFileService(path.get)
      handle <- app(naiveBasicAuthMiddleware(service)).start
      _ <- handle.joinWithNever
    yield ExitCode.Success

Discussion