🥰
Scala の http4s で静的ファイルを配信する(Basic 認証もあるヨ)
最小構成(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