From 2e384f10a4b51fd38b0eb388a3faa977fdc6d4d4 Mon Sep 17 00:00:00 2001 From: trett Date: Thu, 12 Feb 2026 19:29:54 +0100 Subject: [PATCH 1/6] gcp deploy --- DEPLOY.md | 144 ++++++++++++++++++ build.sbt | 28 +++- client/site.webmanifest | 2 +- collector-config.yaml | 22 +++ scripts/local-docker/docker-compose.yml | 2 +- server/src/main/resources/application.conf | 13 +- .../scala/ru/trett/rss/server/Server.scala | 71 +++++---- .../rss/server/authorization/AuthFilter.scala | 18 +-- .../rss/server/authorization/JwtManager.scala | 22 +++ .../server/authorization/SessionManager.scala | 32 ---- .../trett/rss/server/config/AppConfig.scala | 8 +- .../server/controllers/JobController.scala | 44 ++++++ .../server/controllers/LoginController.scala | 14 +- .../server/controllers/LogoutController.scala | 26 ++-- .../server/controllers/UserController.scala | 6 +- .../repositories/ChannelRepository.scala | 2 +- .../server/repositories/FeedRepository.scala | 8 +- .../rss/server/services/UpdateTask.scala | 48 ------ 18 files changed, 346 insertions(+), 164 deletions(-) create mode 100644 DEPLOY.md create mode 100644 collector-config.yaml create mode 100644 server/src/main/scala/ru/trett/rss/server/authorization/JwtManager.scala delete mode 100644 server/src/main/scala/ru/trett/rss/server/authorization/SessionManager.scala create mode 100644 server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala delete mode 100644 server/src/main/scala/ru/trett/rss/server/services/UpdateTask.scala diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..7eb43b5 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,144 @@ +# Deployment Guide for Google Cloud Run + +This guide outlines the steps to deploy the RSS Reader application to Google Cloud Run, including setting up the PostgreSQL database, background update tasks, and monitoring with OpenTelemetry sidecar. + +## Prerequisites + +* **Google Cloud Platform Account** with billing enabled. +* **gcloud CLI** installed and authenticated. +* **Existing Docker Image** hosted in Google Container Registry (GCR) or Artifact Registry. + +## 1. Environment Setup + +Define the following environment variables for your deployment. + +```bash +export PROJECT_ID="your-gcp-project-id" +export REGION="us-central1" # Or your preferred region +export IMAGE_URL="docker.pkg.dev/your-gcp-project-id/your-repo/your-image:latest" +export DB_INSTANCE_NAME="rss-postgres" +export DB_NAME="rss" +export DB_USER="rss_user" +export DB_PASSWORD="your-secure-password" +export SERVICE_NAME="rss-reader" +export JOB_TOKEN="your-secret-job-token" # Generate a strong random string +export OAUTH_CLIENT_ID="your-google-oauth-client-id" +export OAUTH_CLIENT_SECRET="your-google-oauth-client-secret" +export GOOGLE_API_KEY="your-google-gemini-api-key" +``` + +## 2. Infrastructure Setup + +### Create Cloud SQL Instance + +Create a PostgreSQL instance (if you haven't already). + +```bash +gcloud sql instances create $DB_INSTANCE_NAME \ + --database-version=POSTGRES_15 \ + --cpu=1 \ + --memory=3840MiB \ + --region=$REGION +``` + +Create the database and user. + +```bash +gcloud sql databases create $DB_NAME --instance=$DB_INSTANCE_NAME + +gcloud sql users create $DB_USER \ + --instance=$DB_INSTANCE_NAME \ + --password=$DB_PASSWORD +``` + +## 3. Configuration + +### Collector Config +Ensure you have the `collector-config.yaml` file in your current directory. This file configures the OpenTelemetry collector to receive metrics from your app and push them to Google Managed Prometheus. + +### Service Definition +Create a `service.yaml` file with the following content. This defines the multi-container deployment (sidecar). + +**Important:** Replace placeholders (like `YOUR_PROJECT_ID`, `YOUR_IMAGE_URL`) with your actual values or use `envsubst`. + +```yaml +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: rss-reader + annotations: + run.googleapis.com/maxScale: '1' + run.googleapis.com/launch-stage: BETA +spec: + template: + metadata: + annotations: + run.googleapis.com/cloudsql-instances: PROJECT_ID:REGION:DB_INSTANCE_NAME + run.googleapis.com/execution-environment: gen1 + run.googleapis.com/startup-cpu-boost: 'true' + spec: + containers: + - image: IMAGE_URL + name: rss-reader-app + ports: + - containerPort: 8080 + env: + - name: DATASOURCE_URL + value: "jdbc:postgresql:///rss?cloudSqlInstance=$PROJECT_ID:$REGION:$DB_INSTANCE_NAME&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=rss_user&password=$DB_PASSWORD" + - name: CLIENT_ID + value: "OAUTH_CLIENT_ID" + - name: CLIENT_SECRET + value: "OAUTH_CLIENT_SECRET" + - name: GOOGLE_API_KEY + value: "GOOGLE_API_KEY" + - name: JOB_TOKEN + value: "JOB_TOKEN" + - name: SERVER_URL + value: "https://rss-reader-PROJECT_ID.REGION.run.app" # Update after first deploy if needed + - name: CORS_URL + value: "https://rss-reader-PROJECT_ID.REGION.run.app" # Update after first deploy if needed + - name: OTEL_METRICS_EXPORTER + value: none # do nt use OTEL in GCP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "http://localhost:4317" + resources: + limits: + cpu: 1000m + memory: 512Mi + startupProbe: + failureThreshold: 1 + periodSeconds: 240 + tcpSocket: + port: 8080 + timeoutSeconds: 240 + timeoutSeconds: 300 +``` + +## 3. Deploy to Cloud Run + +Deploy using the `service.yaml`. + +```bash +gcloud run services replace service.deploy.yaml --region=$REGION +``` + +## 5. Configure Cloud Scheduler + +Set up a job to trigger the feed update every 10 minutes. + +```bash +# Get the Service URL +SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --platform managed --region $REGION --format "value(status.url)") + +gcloud scheduler jobs create http rss-update-job \ + --schedule="*/10 * * * *" \ + --uri="$SERVICE_URL/api/jobs/update" \ + --http-method=POST \ + --headers="Authorization=Bearer $JOB_TOKEN" \ + --location=$REGION \ + --description="Trigger RSS feed updates" +``` + +## 6. Database Migrations + +The application runs Flyway migrations automatically on startup. No manual schema application is required. diff --git a/build.sbt b/build.sbt index 011c9bb..40066ce 100644 --- a/build.sbt +++ b/build.sbt @@ -1,10 +1,11 @@ import com.typesafe.sbt.SbtNativePackager.autoImport.NativePackagerHelper.* -import com.typesafe.sbt.packager.docker.Cmd +import com.typesafe.sbt.packager.docker.* import org.scalajs.linker.interface.ModuleSplitStyle import scala.sys.process.* -lazy val projectVersion = "2.4.3" +lazy val projectVersion = "2.4.4-gcr" +lazy val gitCommitHash = Process("git rev-parse --short HEAD").!!.trim lazy val organizationName = "ru.trett" lazy val scala3Version = "3.7.4" lazy val circeVersion = "0.14.15" @@ -74,7 +75,26 @@ lazy val server = project organization := organizationName, scalaVersion := scala3Version, name := "server", - dockerBaseImage := "eclipse-temurin:21-jre-jammy", + dockerPermissionStrategy := DockerPermissionStrategy.None, + dockerBaseImage := "gcr.io/distroless/java21", + dockerCommands := { + val commands = dockerCommands.value + val filteredCommands = commands.filter { + case Cmd("RUN", _*) => false + case Cmd("USER", _*) => false + case Cmd("ENTRYPOINT", _*) => false + case Cmd("CMD", _*) => false + case Cmd("WORKDIR", _*) => false + case ExecCmd("ENTRYPOINT", _*) => false + case ExecCmd("CMD", _*) => false + case _ => true + } + filteredCommands ++ Seq( + Cmd("WORKDIR", "/opt/docker"), + ExecCmd("ENTRYPOINT", "java", "-cp", "/opt/docker/lib/*", "ru.trett.rss.server.Server") + ) + }, + Docker / version := gitCommitHash, dockerRepository := sys.env.get("REGISTRY"), dockerExposedPorts := Seq(8080), watchSources ++= (client / Compile / watchSources).value, @@ -121,6 +141,8 @@ lazy val server = project libraryDependencies += "org.jsoup" % "jsoup" % "1.21.2", libraryDependencies += "com.github.blemale" %% "scaffeine" % "5.3.0", libraryDependencies += "io.circe" %% "circe-fs2" % "0.14.1", + libraryDependencies += "com.github.jwt-scala" %% "jwt-circe" % "10.0.1", + libraryDependencies += "com.google.cloud.sql" % "postgres-socket-factory" % "1.15.1", libraryDependencies += "org.flywaydb" % "flyway-database-postgresql" % "11.17.2" % "runtime", libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test, libraryDependencies += "org.scalamock" %% "scalamock" % "7.5.2" % Test, diff --git a/client/site.webmanifest b/client/site.webmanifest index 7fd5804..bd8bf04 100644 --- a/client/site.webmanifest +++ b/client/site.webmanifest @@ -1 +1 @@ -{"name":"RSS Reader","short_name":"RSS","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{"name":"RSS Reader","short_name":"RSS","icons":[{"src":"/images/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/images/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/collector-config.yaml b/collector-config.yaml new file mode 100644 index 0000000..2f3f4bb --- /dev/null +++ b/collector-config.yaml @@ -0,0 +1,22 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +processors: + batch: + +exporters: + googlemanagedprometheus: + debug: + verbosity: normal + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [googlemanagedprometheus, debug] diff --git a/scripts/local-docker/docker-compose.yml b/scripts/local-docker/docker-compose.yml index 5bd2bc7..87aec9d 100644 --- a/scripts/local-docker/docker-compose.yml +++ b/scripts/local-docker/docker-compose.yml @@ -24,7 +24,7 @@ services: - host.docker.internal:host-gateway server: - image: server:2.4.3 + image: server:9bb2749 container_name: rss_server restart: always depends_on: diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf index 76fc97d..9a52557 100644 --- a/server/src/main/resources/application.conf +++ b/server/src/main/resources/application.conf @@ -1,5 +1,6 @@ server { - port = 80 + port = 8080 + port = ${?PORT} port = ${?SERVER_PORT} host = "0.0.0.0" } @@ -32,3 +33,13 @@ google { api-key = "" api-key = ${?GOOGLE_API_KEY} } + +jobs { + token = "" + token = ${?JOB_TOKEN} +} + +jwt { + secret = "change-me-in-production" + secret = ${?JWT_SECRET} +} diff --git a/server/src/main/scala/ru/trett/rss/server/Server.scala b/server/src/main/scala/ru/trett/rss/server/Server.scala index 7c653cb..a0abd19 100644 --- a/server/src/main/scala/ru/trett/rss/server/Server.scala +++ b/server/src/main/scala/ru/trett/rss/server/Server.scala @@ -2,6 +2,7 @@ package ru.trett.rss.server import cats.data.OptionT import cats.effect.* +import cats.effect.unsafe.IORuntimeConfig import cats.implicits.* import com.comcast.ip4s.* import com.zaxxer.hikari.HikariConfig @@ -31,13 +32,14 @@ import org.typelevel.otel4s.oteljava.OtelJava import pureconfig.ConfigSource import scala.concurrent.duration.* import ru.trett.rss.server.authorization.AuthFilter -import ru.trett.rss.server.authorization.SessionManager +import ru.trett.rss.server.authorization.JwtManager import ru.trett.rss.server.config.AppConfig import ru.trett.rss.server.config.CorsConfig import ru.trett.rss.server.config.DbConfig import ru.trett.rss.server.config.OAuthConfig import ru.trett.rss.server.controllers.ChannelController import ru.trett.rss.server.controllers.FeedController +import ru.trett.rss.server.controllers.JobController import ru.trett.rss.server.controllers.LoginController import ru.trett.rss.server.controllers.LogoutController import ru.trett.rss.server.controllers.SummarizeController @@ -50,11 +52,13 @@ import ru.trett.rss.server.repositories.UserRepository import ru.trett.rss.server.services.ChannelService import ru.trett.rss.server.services.FeedService import ru.trett.rss.server.services.SummarizeService -import ru.trett.rss.server.services.UpdateTask import ru.trett.rss.server.services.UserService object Server extends IOApp: + override protected def runtimeConfig: IORuntimeConfig = + super.runtimeConfig.copy(cpuStarvationCheckInitialDelay = Duration.Inf) + private val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger @@ -88,14 +92,14 @@ object Server extends IOApp: .surround { val client = EmberClientBuilder .default[IO] - .withTimeout(120.seconds) // Increased timeout for AI API calls + .withTimeout(30.seconds) .build transactor(appConfig.db).use { xa => client.use { client => for { _ <- FlywayMigration.migrate(appConfig.db) corsPolicy = createCorsPolicy(appConfig.cors) - sessionManager <- SessionManager[IO] + jwtManager = JwtManager(appConfig.jwt.secret) channelRepository = ChannelRepository(xa) feedRepository = FeedRepository(xa) feedService = FeedService(feedRepository) @@ -111,12 +115,17 @@ object Server extends IOApp: "Starting server on port: " + appConfig.server.port ) authFilter <- AuthFilter[IO] + jobController = new JobController( + channelService, + userService, + appConfig.jobs + ) jarRoutes <- resourceServiceBuilder[IO]("/public").toRoutes appRoutes <- corsPolicy( jarRoutes <+> routes( - sessionManager, + jwtManager, channelService, userService, feedService, @@ -124,28 +133,19 @@ object Server extends IOApp: authFilter, client, summarizeService, - new LogoutController[IO](sessionManager) + new LogoutController[IO], + jobController ) ) - exitCode <- UpdateTask( - channelService, - userService - ).background.void - .surround { - for { - server <- EmberServerBuilder - .default[IO] - .withHost(ipv4"0.0.0.0") - .withPort( - Port.fromInt(appConfig.server.port).get - ) - .withHttpApp( - withErrorLogging(appRoutes).orNotFound - ) - .build - .use(_ => IO.never) - } yield server - } + exitCode <- + EmberServerBuilder + .default[IO] + .withHost(ipv4"0.0.0.0") + .withPort(Port.fromInt(appConfig.server.port).get) + .withHttpApp(withErrorLogging(appRoutes).orNotFound) + .build + .use(_ => IO.never) + .as(ExitCode.Success) } yield exitCode } } @@ -185,7 +185,7 @@ object Server extends IOApp: .withMaxAge(config.maxAge) private def routes( - sessionManager: SessionManager[IO], + jwtManager: JwtManager, channelService: ChannelService, userService: UserService, feedService: FeedService, @@ -193,10 +193,11 @@ object Server extends IOApp: authFilter: AuthFilter[IO], client: Client[IO], summarizeService: SummarizeService, - logoutController: LogoutController[IO] + logoutController: LogoutController[IO], + jobController: JobController ): HttpRoutes[IO] = - unprotectedRoutes(sessionManager, oauthConfig, userService, client) <+> - authFilter.middleware(sessionManager, userService)( + unprotectedRoutes(jwtManager, oauthConfig, userService, client, jobController) <+> + authFilter.middleware(jwtManager, userService)( authedRoutes( channelService, userService, @@ -213,12 +214,18 @@ object Server extends IOApp: } private def unprotectedRoutes( - sessionManager: SessionManager[IO], + jwtManager: JwtManager, oauthConfig: OAuthConfig, userService: UserService, - client: Client[IO] + client: Client[IO], + jobController: JobController ): HttpRoutes[IO] = - LoginController.routes(sessionManager, oauthConfig, userService, client) <+> indexRoute + LoginController.routes( + jwtManager, + oauthConfig, + userService, + client + ) <+> indexRoute <+> jobController.routes private def authedRoutes( channelService: ChannelService, diff --git a/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala b/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala index fefe8f2..223f054 100644 --- a/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala +++ b/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala @@ -13,27 +13,23 @@ import scala.concurrent.duration.* class AuthFilter[F[_]: Sync: LiftIO] private (cache: Cache[String, User]): - def middleware( - sessionManager: SessionManager[F], - userService: UserService - ): AuthMiddleware[F, User] = - AuthMiddleware(authUser(sessionManager, userService)) + def middleware(jwtManager: JwtManager, userService: UserService): AuthMiddleware[F, User] = + AuthMiddleware(authUser(jwtManager, userService)) def updateCache(user: User): F[Unit] = Sync[F].delay(cache.put(user.email, user)) private def authUser( - sessionManager: SessionManager[F], + jwtManager: JwtManager, userService: UserService ): Kleisli[[A] =>> OptionT[F, A], Request[F], User] = Kleisli { req => req.cookies.find(_.name == "sessionId") match { case Some(sessionId) => - OptionT - .some(sessionId.content) - .flatMapF(sessionManager.getSession) - .flatMap(sessionData => + jwtManager.verifyToken(sessionId.content) match { + case Right(sessionData) => OptionT(getUser(sessionData.userEmail, userService)) - ) + case Left(_) => OptionT.none[F, User] + } case None => OptionT.none[F, User] } } diff --git a/server/src/main/scala/ru/trett/rss/server/authorization/JwtManager.scala b/server/src/main/scala/ru/trett/rss/server/authorization/JwtManager.scala new file mode 100644 index 0000000..e2d848a --- /dev/null +++ b/server/src/main/scala/ru/trett/rss/server/authorization/JwtManager.scala @@ -0,0 +1,22 @@ +package ru.trett.rss.server.authorization + +import pdi.jwt.{JwtAlgorithm, JwtCirce, JwtClaim} +import io.circe.syntax.* +import io.circe.generic.auto.* +import java.time.Clock + +case class SessionData(userEmail: String) + +class JwtManager(secret: String): + private val algorithm = JwtAlgorithm.HS256 + private val clock: Clock = Clock.systemUTC() + + def createToken(data: SessionData): String = + val claim = + JwtClaim(data.asJson.noSpaces).issuedNow(clock).expiresIn(60 * 60 * 24)(clock) // 1 day + JwtCirce.encode(claim, secret, algorithm) + + def verifyToken(token: String): Either[Throwable, SessionData] = + JwtCirce.decodeJson(token, secret, Seq(algorithm)).toEither.flatMap { json => + json.as[SessionData] + } diff --git a/server/src/main/scala/ru/trett/rss/server/authorization/SessionManager.scala b/server/src/main/scala/ru/trett/rss/server/authorization/SessionManager.scala deleted file mode 100644 index 67ab831..0000000 --- a/server/src/main/scala/ru/trett/rss/server/authorization/SessionManager.scala +++ /dev/null @@ -1,32 +0,0 @@ -package ru.trett.rss.server.authorization - -import cats.effect.* -import cats.effect.std.UUIDGen -import cats.syntax.all.* -import com.github.blemale.scaffeine.{Cache, Scaffeine} - -import scala.concurrent.duration.DurationInt - -case class SessionData(userEmail: String) - -class SessionManager[F[_]: Sync] private (cache: Cache[String, SessionData]): - - def createSession(data: SessionData): F[String] = - for { - sessionId <- UUIDGen.randomString[F] - _ <- Sync[F].delay(cache.put(sessionId, data)) - } yield sessionId - - def getSession(sessionId: String): F[Option[SessionData]] = - Sync[F].delay(cache.getIfPresent(sessionId)) - - def deleteSession(sessionId: String): F[Unit] = - Sync[F].delay(cache.invalidate(sessionId)) - -object SessionManager: - def apply[F[_]: Sync]: F[SessionManager[F]] = - val cache: Cache[String, SessionData] = Scaffeine() - .maximumSize(500) - .expireAfterWrite(1.days) - .build[String, SessionData]() - new SessionManager(cache).pure[F] diff --git a/server/src/main/scala/ru/trett/rss/server/config/AppConfig.scala b/server/src/main/scala/ru/trett/rss/server/config/AppConfig.scala index 569b1f8..6394a03 100644 --- a/server/src/main/scala/ru/trett/rss/server/config/AppConfig.scala +++ b/server/src/main/scala/ru/trett/rss/server/config/AppConfig.scala @@ -9,7 +9,9 @@ case class AppConfig( db: DbConfig, oauth: OAuthConfig, cors: CorsConfig, - google: GoogleConfig + google: GoogleConfig, + jobs: JobConfig, + jwt: JwtConfig ) derives ConfigReader case class ServerConfig(port: Int, host: String = "0.0.0.0") derives ConfigReader @@ -24,3 +26,7 @@ case class CorsConfig(allowedOrigin: String, allowCredentials: Boolean, maxAge: derives ConfigReader case class GoogleConfig(apiKey: String) derives ConfigReader + +case class JobConfig(token: String) derives ConfigReader + +case class JwtConfig(secret: String) derives ConfigReader diff --git a/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala b/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala new file mode 100644 index 0000000..8b4b682 --- /dev/null +++ b/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala @@ -0,0 +1,44 @@ +package ru.trett.rss.server.controllers + +import cats.effect.IO +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.dsl.io.* +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.LoggerFactory +import ru.trett.rss.server.config.JobConfig +import ru.trett.rss.server.services.ChannelService +import ru.trett.rss.server.services.UserService +import org.typelevel.ci.* + +class JobController(channelService: ChannelService, userService: UserService, config: JobConfig)( + using loggerFactory: LoggerFactory[IO] +): + private val logger: Logger[IO] = loggerFactory.getLogger + + def routes: HttpRoutes[IO] = HttpRoutes.of[IO] { + case req @ POST -> Root / "api" / "jobs" / "update" => + val token = req.headers.get(ci"Authorization").map(_.head.value) + if (config.token.nonEmpty && !token.contains(s"Bearer ${config.token}")) { + logger.warn("Unauthorized job update attempt") *> Forbidden("Invalid token") + } else { + for { + _ <- logger.info("Starting scheduled feed update") + users <- userService.getUsers + _ <- logger.info(s"Found ${users.size} users") + channelCounts <- users.parTraverse { user => + for { + channels <- channelService.updateFeeds(user) + _ <- logger.info( + s"Updated ${channels.size} channels for user ${user.email}" + ) + } yield channels.size + } + total = channelCounts.sum + _ <- logger.info( + s"Finished scheduled update. Updated $total channels in total." + ) + resp <- Ok(s"Updated $total channels") + } yield resp + } + } diff --git a/server/src/main/scala/ru/trett/rss/server/controllers/LoginController.scala b/server/src/main/scala/ru/trett/rss/server/controllers/LoginController.scala index 13b53a2..2230a62 100644 --- a/server/src/main/scala/ru/trett/rss/server/controllers/LoginController.scala +++ b/server/src/main/scala/ru/trett/rss/server/controllers/LoginController.scala @@ -19,7 +19,7 @@ object LoginController: Uri.unsafeFromString("https://accounts.google.com/o/oauth2/v2/auth") def routes( - sessionManager: SessionManager[IO], + jwtManager: JwtManager, oauthConfig: OAuthConfig, userService: UserService, client: Client[IO] @@ -59,13 +59,14 @@ object LoginController: ) userInfo <- getUserInfo(client, token.access_token) sessionData = SessionData(userInfo.email) - sessionId <- sessionManager.createSession(sessionData) + jwtToken = jwtManager.createToken(sessionData) response <- SeeOther(Location(uri"/")) .map( _.addCookie( ResponseCookie( "sessionId", - sessionId, + jwtToken, + path = Some("/"), httpOnly = true, secure = true, maxAge = Some(60 * 60 * 24) // 1 day @@ -90,13 +91,6 @@ object LoginController: case Left(_) => BadRequest("Failed to create user") } } yield response - - case req @ POST -> Root / "logout" => - req.cookies.find(_.name == "sessionId") match { - case Some(cookie) => - sessionManager.deleteSession(cookie.content) *> Ok("Logged out") - case None => BadRequest("No session to log out from") - } } private def getToken( diff --git a/server/src/main/scala/ru/trett/rss/server/controllers/LogoutController.scala b/server/src/main/scala/ru/trett/rss/server/controllers/LogoutController.scala index 1492446..b14ddbf 100644 --- a/server/src/main/scala/ru/trett/rss/server/controllers/LogoutController.scala +++ b/server/src/main/scala/ru/trett/rss/server/controllers/LogoutController.scala @@ -2,31 +2,25 @@ package ru.trett.rss.server.controllers import cats.effect.Async import org.http4s.dsl.Http4sDsl -import ru.trett.rss.server.authorization.SessionManager import cats.implicits._ import org.http4s.AuthedRoutes import org.typelevel.log4cats.LoggerFactory import ru.trett.rss.server.models.User +import org.http4s.ResponseCookie -class LogoutController[F[_]: Async: LoggerFactory](sessionManager: SessionManager[F]) - extends Http4sDsl[F] { +class LogoutController[F[_]: Async: LoggerFactory] extends Http4sDsl[F] { private val logger = LoggerFactory[F].getLogger val routes: AuthedRoutes[User, F] = AuthedRoutes.of { case authReq @ POST -> Root / "api" / "logout" as user => - authReq.req.cookies.find(_.name == "sessionId") match { - case Some(cookie) => - for { - _ <- logger.info(s"Deleting session for user: ${user.email}") - _ <- sessionManager.deleteSession(cookie.content) - res <- Ok() - } yield res - case None => - logger.warn( - s"Logout request without session cookie for user: ${user.email}" - ) >> Ok() - } + for { + _ <- logger.info(s"Logging out user: ${user.email}") + res <- Ok().map( + _.addCookie( + ResponseCookie("sessionId", "", path = Some("/"), maxAge = Some(-1)) + ) + ) + } yield res } - } diff --git a/server/src/main/scala/ru/trett/rss/server/controllers/UserController.scala b/server/src/main/scala/ru/trett/rss/server/controllers/UserController.scala index a8544b0..1a0bc0b 100644 --- a/server/src/main/scala/ru/trett/rss/server/controllers/UserController.scala +++ b/server/src/main/scala/ru/trett/rss/server/controllers/UserController.scala @@ -46,10 +46,10 @@ object UserController { ) ) result <- userService.updateUserSettings(updatedUser) - _ <- logger.info(s"""User: ${user.email} was updated with settings: - |hideRead: ${settings.hideRead}, + _ <- logger.info(s"""User: ${user.email} was updated with settings: + |hideRead: ${settings.hideRead}, |summaryLanguage: $validatedLanguage, - |aiMode: ${settings.aiMode}, + |aiMode: ${settings.aiMode}, |summaryModel: $validatedModel""".stripMargin) _ <- cacheUpdater(updatedUser) response <- Ok(s"User created with result: $result") diff --git a/server/src/main/scala/ru/trett/rss/server/repositories/ChannelRepository.scala b/server/src/main/scala/ru/trett/rss/server/repositories/ChannelRepository.scala index 6779d52..fc171a4 100644 --- a/server/src/main/scala/ru/trett/rss/server/repositories/ChannelRepository.scala +++ b/server/src/main/scala/ru/trett/rss/server/repositories/ChannelRepository.scala @@ -19,7 +19,7 @@ class ChannelRepository(xa: Transactor[IO]): def insertChannel(channel: Channel, user: User): IO[Long] = val insertChannelQuery = sql""" - INSERT INTO channels (title, link) + INSERT INTO channels (title, link) VALUES (${channel.title}, ${channel.link}) """.update.withUniqueGeneratedKeys[Long]("id") diff --git a/server/src/main/scala/ru/trett/rss/server/repositories/FeedRepository.scala b/server/src/main/scala/ru/trett/rss/server/repositories/FeedRepository.scala index 60436b6..6bad960 100644 --- a/server/src/main/scala/ru/trett/rss/server/repositories/FeedRepository.scala +++ b/server/src/main/scala/ru/trett/rss/server/repositories/FeedRepository.scala @@ -29,15 +29,15 @@ class FeedRepository(xa: Transactor[IO]): def getUnreadCount(channelId: Long, userId: String): IO[Int] = sql""" - SELECT COUNT(*) - FROM feeds + SELECT COUNT(*) + FROM feeds WHERE channel_id = $channelId AND user_id = $userId AND read = false """.query[Int].unique.transact(xa) def getTotalUnreadCount(userId: String): IO[Int] = sql""" - SELECT COUNT(*) - FROM feeds + SELECT COUNT(*) + FROM feeds WHERE user_id = $userId AND read = false """.query[Int].unique.transact(xa) diff --git a/server/src/main/scala/ru/trett/rss/server/services/UpdateTask.scala b/server/src/main/scala/ru/trett/rss/server/services/UpdateTask.scala deleted file mode 100644 index a88f00c..0000000 --- a/server/src/main/scala/ru/trett/rss/server/services/UpdateTask.scala +++ /dev/null @@ -1,48 +0,0 @@ -package ru.trett.rss.server.services - -import cats.effect.IO -import cats.syntax.all.* -import fs2.Stream -import org.typelevel.log4cats.Logger -import org.typelevel.log4cats.LoggerFactory - -import scala.concurrent.duration.* - -class UpdateTask private (channelService: ChannelService, userService: UserService)(using - loggerFactory: LoggerFactory[IO] -): - - private val logger: Logger[IO] = loggerFactory.getLogger - - private def updateChannels(): IO[Int] = { - for { - users <- userService.getUsers - _ <- logger.info(s"Found ${users.size} users") - channelCounts <- users.parTraverse { user => - for { - channels <- channelService.updateFeeds(user) - _ <- logger.info(s"Updated ${channels.size} channels for user ${user.email}") - } yield channels.size - } - } yield channelCounts.sum - } - - private def taskStream: Stream[IO, Int] = - Stream - .awakeEvery[IO](10.minutes) - .evalMap(_ => updateChannels()) - .handleErrorWith { error => - Stream.exec(logger.error(error)("Stream failed, restarting")) - } - - private def job: Stream[IO, Unit] = - Stream.bracket(logger.info("Starting background job"))(_ => - logger.info("Stopping background job") - ) >> taskStream.drain - -object UpdateTask: - - def apply(channelService: ChannelService, userService: UserService)(using - loggerFactory: LoggerFactory[IO] - ): IO[Unit] = - new UpdateTask(channelService, userService).job.compile.drain From 34e43e881bafeb1c43beafd77ff857ecbc96093e Mon Sep 17 00:00:00 2001 From: trett Date: Mon, 16 Feb 2026 10:37:15 +0100 Subject: [PATCH 2/6] remove otel --- DEPLOY.md | 9 +- README.md | 43 +++--- build.sbt | 13 -- scripts/local-docker/docker-compose.yml | 37 ----- .../scala/ru/trett/rss/server/Server.scala | 133 ++++++++---------- 5 files changed, 81 insertions(+), 154 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 7eb43b5..dfbcaad 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,6 +1,6 @@ # Deployment Guide for Google Cloud Run -This guide outlines the steps to deploy the RSS Reader application to Google Cloud Run, including setting up the PostgreSQL database, background update tasks, and monitoring with OpenTelemetry sidecar. +This guide outlines the steps to deploy the RSS Reader application to Google Cloud Run, including setting up the PostgreSQL database and background update tasks. ## Prerequisites @@ -53,9 +53,6 @@ gcloud sql users create $DB_USER \ ## 3. Configuration -### Collector Config -Ensure you have the `collector-config.yaml` file in your current directory. This file configures the OpenTelemetry collector to receive metrics from your app and push them to Google Managed Prometheus. - ### Service Definition Create a `service.yaml` file with the following content. This defines the multi-container deployment (sidecar). @@ -97,10 +94,6 @@ spec: value: "https://rss-reader-PROJECT_ID.REGION.run.app" # Update after first deploy if needed - name: CORS_URL value: "https://rss-reader-PROJECT_ID.REGION.run.app" # Update after first deploy if needed - - name: OTEL_METRICS_EXPORTER - value: none # do nt use OTEL in GCP - - name: OTEL_EXPORTER_OTLP_ENDPOINT - value: "http://localhost:4317" resources: limits: cpu: 1000m diff --git a/README.md b/README.md index 5827fea..b99a8c4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # RSS Reader -A modern, web-based RSS reader application built with Scala, Scala.js, and Laminar. It provides a simple and clean interface for reading your favorite RSS feeds, with the ability to generate summaries of unread articles. +A modern, containerized RSS reader application built with Scala, Scala.js, and Laminar. Designed for cloud-native deployment (optimized for Google Cloud Run), it provides a simple and clean interface for reading your favorite RSS feeds, with AI-powered summaries of unread articles. ## Features @@ -12,7 +12,6 @@ A modern, web-based RSS reader application built with Scala, Scala.js, and Lamin - **AI-Powered Summaries**: Generate summaries of all your unread articles using Google's Generative AI. - **Secure Authentication**: Authentication is handled securely via Google OAuth2. - **Responsive Design**: The application is designed to work on both desktop and mobile browsers. -- **Observability**: Built-in metrics collection with OpenTelemetry, exportable to Prometheus and visualizable in Grafana. ## Tech Stack @@ -25,9 +24,6 @@ A modern, web-based RSS reader application built with Scala, Scala.js, and Lamin - [Flyway](https://flywaydb.org/): For database migrations. - [circe](https://circe.github.io/circe/): For JSON manipulation. - [PureConfig](https://pureconfig.github.io/): For loading configuration. -- [OpenTelemetry](https://opentelemetry.io/): For metrics collection and observability. -- [Prometheus](https://prometheus.io/): For metrics storage and querying. -- [Grafana](https://grafana.com/): For metrics visualization and dashboards. ### Frontend @@ -45,12 +41,12 @@ A modern, web-based RSS reader application built with Scala, Scala.js, and Lamin - [Node.js 20](https://nodejs.org/) and [npm](https://www.npmjs.com/) (version specified in `.nvmrc`) - [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) -### Running with Docker (Recommended) +### Running Locally with Docker -This is the easiest way to run the application. +This is the easiest way to test the application locally in a production-like environment. 1. **Set up environment variables**: - You'll need to provide your Google OAuth credentials. Create a `.env` file in the `scripts/local-docker` directory with the following content: + You'll need to provide your Google OAuth credentials and Gemini API key. Create a `.env` file in the `scripts/local-docker` directory with the following content: ``` CLIENT_ID=your_google_client_id CLIENT_SECRET=your_google_client_secret @@ -58,25 +54,22 @@ This is the easiest way to run the application. ``` 2. **Build Docker images**: - This command will build the Docker image. + This command will build the Docker image locally. ```bash sbt buildImage ``` 3. **Run the application**: - Use Docker Compose to start all the services. + Use Docker Compose to start all the services (App, Postgres, Caddy). ```bash docker-compose -f scripts/local-docker/docker-compose.yml up ``` The application will be available at: - Main app: `http://localhost` - - Prometheus: `http://localhost:9090` - - Grafana: `http://localhost:3000` (default credentials: admin/admin) - - Metrics endpoint: `http://localhost:9464/metrics` ### Local Development -This setup is for actively developing the application. +This setup is for actively developing the application with hot-reloading where possible. 1. **Start the database**: Prepare and start a PostgreSQL database instance. @@ -107,16 +100,28 @@ The application is configured using environment variables. | `SERVER_URL` | The public URL of the server. Used for OAuth redirect URI. | `https://localhost` | No | | `CORS_URL` | The allowed origin for CORS requests. | `https://localhost` | No | | `GOOGLE_API_KEY` | The API key for Google's Generative AI. | - | For summary feature | +| `JOB_TOKEN` | Secret token for triggering background jobs via HTTP. | - | No | | `REGISTRY` | The Docker registry to push the image to | - | No | ## Deployment -The Docker image built with `sbt buildImage` can be used for production deployment. The image includes both the backend server and frontend assets. You can adapt the `scripts/local-docker/docker-compose.yml` file for your production environment. Remember to configure all the necessary environment variables. +The application is optimized for deployment as a **Google Cloud Run** service. The container image includes both the backend server and the pre-built frontend assets. -To push the image to a registry, set the `REGISTRY` environment variable and run: -```bash -sbt pushImage -``` +For a comprehensive step-by-step deployment guide, including Cloud SQL and Cloud Scheduler setup, please refer to **[DEPLOY.md](DEPLOY.md)**. + +### Building and Pushing to Registry + +To build the production image and push it to your configured container registry: + +1. Set the `REGISTRY` environment variable: + ```bash + export REGISTRY=docker.pkg.dev/your-project/your-repo/rss-reader + ``` + +2. Run the push command: + ```bash + sbt pushImage + ``` ## License diff --git a/build.sbt b/build.sbt index 40066ce..2eb3d30 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,6 @@ lazy val scala3Version = "3.7.4" lazy val circeVersion = "0.14.15" lazy val htt4sVersion = "1.0.0-M45" lazy val logs4catVersion = "2.7.1" -lazy val otel4sVersion = "0.14.0" lazy val doobieVersion = "1.0.0-RC11" lazy val customScalaOptions = Seq("-Wunused:imports", "-rewrite", "-source:3.4-migration") @@ -98,7 +97,6 @@ lazy val server = project dockerRepository := sys.env.get("REGISTRY"), dockerExposedPorts := Seq(8080), watchSources ++= (client / Compile / watchSources).value, - javaOptions += "-Dotel.java.global-autoconfigure.enabled=true", libraryDependencies ++= Seq( "org.typelevel" %% "cats-effect" % "3.6.3", "org.slf4j" % "slf4j-api" % "2.0.17", @@ -116,17 +114,6 @@ lazy val server = project "org.typelevel" %% "log4cats-core", "org.typelevel" %% "log4cats-slf4j" ).map(_ % logs4catVersion), - libraryDependencies ++= Seq( - "org.typelevel" %% "otel4s-oteljava", - "org.typelevel" %% "otel4s-instrumentation-metrics" - ).map(_ % otel4sVersion), - libraryDependencies ++= Seq( - "org.typelevel" %% "otel4s-oteljava" % "0.14.0", - "io.opentelemetry.instrumentation" % "opentelemetry-runtime-telemetry-java17" % "2.22.0-alpha", - "io.opentelemetry" % "opentelemetry-exporter-prometheus" % "1.45.0-alpha", - "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.56.0" % Runtime, - "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.56.0" % Runtime - ), libraryDependencies ++= Seq( "io.circe" %%% "circe-core", "io.circe" %%% "circe-generic", diff --git a/scripts/local-docker/docker-compose.yml b/scripts/local-docker/docker-compose.yml index 87aec9d..aabab61 100644 --- a/scripts/local-docker/docker-compose.yml +++ b/scripts/local-docker/docker-compose.yml @@ -29,8 +29,6 @@ services: restart: always depends_on: - caddy - ports: - - "9464:9464" environment: SERVER_PORT: 8080 DATASOURCE_URL: jdbc:postgresql://postgresdb:5432/rss @@ -40,38 +38,3 @@ services: CLIENT_ID: ${CLIENT_ID} CLIENT_SECRET: ${CLIENT_SECRET} GOOGLE_API_KEY: ${GOOGLE_API_KEY} - OTEL_EXPORTER_PROMETHEUS_PORT: 9464 - OTEL_METRICS_EXPORTER: prometheus - - prometheus: - image: prom/prometheus:latest - container_name: prometheus - restart: always - ports: - - "9090:9090" - volumes: - - $PWD/prometheus.yml:/etc/prometheus/prometheus.yml - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - depends_on: - - server - - grafana: - image: grafana/grafana:latest - container_name: grafana - restart: always - ports: - - "3000:3000" - environment: - - GF_SECURITY_ADMIN_USER=admin - - GF_SECURITY_ADMIN_PASSWORD=admin - volumes: - - grafana-storage:/var/lib/grafana - - $PWD/grafana/provisioning:/etc/grafana/provisioning - depends_on: - - prometheus - -volumes: - grafana-storage: - diff --git a/server/src/main/scala/ru/trett/rss/server/Server.scala b/server/src/main/scala/ru/trett/rss/server/Server.scala index a0abd19..7a5b89b 100644 --- a/server/src/main/scala/ru/trett/rss/server/Server.scala +++ b/server/src/main/scala/ru/trett/rss/server/Server.scala @@ -9,8 +9,6 @@ import com.zaxxer.hikari.HikariConfig import doobie.hikari.* import doobie.util.log.LogEvent import doobie.util.log.LogHandler -import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry} -import io.opentelemetry.instrumentation.runtimemetrics.java17.* import org.http4s.AuthedRoutes import org.http4s.HttpRoutes import org.http4s.StaticFile @@ -27,8 +25,6 @@ import org.http4s.server.middleware.ErrorHandling import org.http4s.server.staticcontent.* import org.typelevel.log4cats.* import org.typelevel.log4cats.slf4j.* -import org.typelevel.otel4s.instrumentation.ce.IORuntimeMetrics -import org.typelevel.otel4s.oteljava.OtelJava import pureconfig.ConfigSource import scala.concurrent.duration.* import ru.trett.rss.server.authorization.AuthFilter @@ -67,11 +63,6 @@ object Server extends IOApp: println(logEvent.sql) } - private def registerRuntimeMetrics(openTelemetry: JOpenTelemetry): Resource[IO, Unit] = { - val acquire = IO.delay(RuntimeMetrics.create(openTelemetry)) - Resource.fromAutoCloseable(acquire).void - } - override def run(args: List[String]): IO[ExitCode] = val appConfig = loadConfig match { case Some(config) => config @@ -80,77 +71,65 @@ object Server extends IOApp: return IO.pure(ExitCode.Error) } - OtelJava - .autoConfigured[IO]() - .flatTap(otel4s => registerRuntimeMetrics(otel4s.underlying)) - .evalTap(_ => logger.info("OpenTelemetry metrics initialized")) - .use { otel4s => - given org.typelevel.otel4s.metrics.MeterProvider[IO] = - otel4s.meterProvider - IORuntimeMetrics - .register[IO](runtime.metrics, IORuntimeMetrics.Config.default) - .surround { - val client = EmberClientBuilder + val client = EmberClientBuilder + .default[IO] + .withTimeout(30.seconds) + .build + transactor(appConfig.db).use { xa => + client.use { client => + for { + _ <- FlywayMigration.migrate(appConfig.db) + corsPolicy = createCorsPolicy(appConfig.cors) + jwtManager = JwtManager(appConfig.jwt.secret) + channelRepository = ChannelRepository(xa) + feedRepository = FeedRepository(xa) + feedService = FeedService(feedRepository) + userRepository = UserRepository(xa) + userService = UserService(userRepository) + summarizeService = new SummarizeService( + feedRepository, + client, + appConfig.google.apiKey + ) + channelService = ChannelService(channelRepository, client) + _ <- logger.info( + "Starting server on port: " + appConfig.server.port + ) + authFilter <- AuthFilter[IO] + jobController = new JobController( + channelService, + userService, + appConfig.jobs + ) + jarRoutes <- resourceServiceBuilder[IO]("/public").toRoutes + appRoutes <- + corsPolicy( + jarRoutes <+> + routes( + jwtManager, + channelService, + userService, + feedService, + appConfig.oauth, + authFilter, + client, + summarizeService, + new LogoutController[IO], + jobController + ) + ) + exitCode <- + EmberServerBuilder .default[IO] - .withTimeout(30.seconds) + .withHost(ipv4"0.0.0.0") + .withPort(Port.fromInt(appConfig.server.port).get) + .withHttpApp(withErrorLogging(appRoutes).orNotFound) .build - transactor(appConfig.db).use { xa => - client.use { client => - for { - _ <- FlywayMigration.migrate(appConfig.db) - corsPolicy = createCorsPolicy(appConfig.cors) - jwtManager = JwtManager(appConfig.jwt.secret) - channelRepository = ChannelRepository(xa) - feedRepository = FeedRepository(xa) - feedService = FeedService(feedRepository) - userRepository = UserRepository(xa) - userService = UserService(userRepository) - summarizeService = new SummarizeService( - feedRepository, - client, - appConfig.google.apiKey - ) - channelService = ChannelService(channelRepository, client) - _ <- logger.info( - "Starting server on port: " + appConfig.server.port - ) - authFilter <- AuthFilter[IO] - jobController = new JobController( - channelService, - userService, - appConfig.jobs - ) - jarRoutes <- resourceServiceBuilder[IO]("/public").toRoutes - appRoutes <- - corsPolicy( - jarRoutes <+> - routes( - jwtManager, - channelService, - userService, - feedService, - appConfig.oauth, - authFilter, - client, - summarizeService, - new LogoutController[IO], - jobController - ) - ) - exitCode <- - EmberServerBuilder - .default[IO] - .withHost(ipv4"0.0.0.0") - .withPort(Port.fromInt(appConfig.server.port).get) - .withHttpApp(withErrorLogging(appRoutes).orNotFound) - .build - .use(_ => IO.never) - .as(ExitCode.Success) - } yield exitCode - } - } - } + .use(_ => IO.never) + .as(ExitCode.Success) + } yield exitCode } + } private def loadConfig: Option[AppConfig] = ConfigSource.default.load[AppConfig] match { From 1eccdca15c6f2ce473c8294c1b99622928a3ebd5 Mon Sep 17 00:00:00 2001 From: trett Date: Mon, 16 Feb 2026 12:29:30 +0100 Subject: [PATCH 3/6] flyway hack --- DEPLOY.md | 21 +- README.md | 3 + build.sbt | 74 +- .../images/android-chrome-192x192.png | Bin .../images/android-chrome-512x512.png | Bin .../{ => public}/images/apple-touch-icon.png | Bin client/{ => public}/images/background.webp | Bin client/{ => public}/images/favicon-16x16.png | Bin client/{ => public}/images/favicon-32x32.png | Bin client/{ => public}/images/favicon.ico | Bin client/{ => public}/site.webmanifest | 0 cloudbuild.yaml | 35 + collector-config.yaml | 22 - scripts/local-docker/docker-compose.yml | 3 +- .../ru.trett.rss/server/jni-config.json | 48 + .../server/predefined-classes-config.json | 8 + .../ru.trett.rss/server/proxy-config.json | 5 + .../ru.trett.rss/server/reflect-config.json | 866 ++++++++++++++++++ .../ru.trett.rss/server/resource-config.json | 35 + server/src/main/resources/application.conf | 3 +- .../scala/ru/trett/rss/server/Server.scala | 13 +- .../rss/server/authorization/AuthFilter.scala | 22 +- .../server/controllers/JobController.scala | 2 +- .../trett/rss/server/db/FlywayMigration.scala | 58 +- .../controllers/JobControllerSpec.scala | 77 ++ .../trett/rss/server/utils/TestDatabase.scala | 14 +- 26 files changed, 1228 insertions(+), 81 deletions(-) rename client/{ => public}/images/android-chrome-192x192.png (100%) rename client/{ => public}/images/android-chrome-512x512.png (100%) rename client/{ => public}/images/apple-touch-icon.png (100%) rename client/{ => public}/images/background.webp (100%) rename client/{ => public}/images/favicon-16x16.png (100%) rename client/{ => public}/images/favicon-32x32.png (100%) rename client/{ => public}/images/favicon.ico (100%) rename client/{ => public}/site.webmanifest (100%) create mode 100644 cloudbuild.yaml delete mode 100644 collector-config.yaml create mode 100644 server/src/main/resources/META-INF/native-image/ru.trett.rss/server/jni-config.json create mode 100644 server/src/main/resources/META-INF/native-image/ru.trett.rss/server/predefined-classes-config.json create mode 100644 server/src/main/resources/META-INF/native-image/ru.trett.rss/server/proxy-config.json create mode 100644 server/src/main/resources/META-INF/native-image/ru.trett.rss/server/reflect-config.json create mode 100644 server/src/main/resources/META-INF/native-image/ru.trett.rss/server/resource-config.json create mode 100644 server/src/test/scala/ru/trett/rss/server/controllers/JobControllerSpec.scala diff --git a/DEPLOY.md b/DEPLOY.md index dfbcaad..360e00b 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -22,6 +22,7 @@ export DB_USER="rss_user" export DB_PASSWORD="your-secure-password" export SERVICE_NAME="rss-reader" export JOB_TOKEN="your-secret-job-token" # Generate a strong random string +export JWT_SECRET="your-secure-jwt-secret" # Generate a strong random string for token signing export OAUTH_CLIENT_ID="your-google-oauth-client-id" export OAUTH_CLIENT_SECRET="your-google-oauth-client-secret" export GOOGLE_API_KEY="your-google-gemini-api-key" @@ -54,7 +55,7 @@ gcloud sql users create $DB_USER \ ## 3. Configuration ### Service Definition -Create a `service.yaml` file with the following content. This defines the multi-container deployment (sidecar). +Create a `service.yaml` file with the following content. This defines the Cloud Run service. **Important:** Replace placeholders (like `YOUR_PROJECT_ID`, `YOUR_IMAGE_URL`) with your actual values or use `envsubst`. @@ -70,7 +71,7 @@ spec: template: metadata: annotations: - run.googleapis.com/cloudsql-instances: PROJECT_ID:REGION:DB_INSTANCE_NAME + run.googleapis.com/cloudsql-instances: ${PROJECT_ID}:${REGION}:${DB_INSTANCE_NAME} run.googleapis.com/execution-environment: gen1 run.googleapis.com/startup-cpu-boost: 'true' spec: @@ -81,15 +82,17 @@ spec: - containerPort: 8080 env: - name: DATASOURCE_URL - value: "jdbc:postgresql:///rss?cloudSqlInstance=$PROJECT_ID:$REGION:$DB_INSTANCE_NAME&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=rss_user&password=$DB_PASSWORD" + value: "jdbc:postgresql:///rss?cloudSqlInstance=$PROJECT_ID:$REGION:$DB_INSTANCE_NAME&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$DB_USER&password=$DB_PASSWORD" - name: CLIENT_ID - value: "OAUTH_CLIENT_ID" + value: "$OAUTH_CLIENT_ID" - name: CLIENT_SECRET - value: "OAUTH_CLIENT_SECRET" + value: "$OAUTH_CLIENT_SECRET" - name: GOOGLE_API_KEY - value: "GOOGLE_API_KEY" + value: "$GOOGLE_API_KEY" - name: JOB_TOKEN - value: "JOB_TOKEN" + value: "$JOB_TOKEN" + - name: JWT_SECRET + value: "$JWT_SECRET" - name: SERVER_URL value: "https://rss-reader-PROJECT_ID.REGION.run.app" # Update after first deploy if needed - name: CORS_URL @@ -107,12 +110,12 @@ spec: timeoutSeconds: 300 ``` -## 3. Deploy to Cloud Run +## 4. Deploy to Cloud Run Deploy using the `service.yaml`. ```bash -gcloud run services replace service.deploy.yaml --region=$REGION +envsubst < service.yaml | gcloud run services replace - --region=$REGION ``` ## 5. Configure Cloud Scheduler diff --git a/README.md b/README.md index b99a8c4..8d28e81 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ This is the easiest way to test the application locally in a production-like env CLIENT_ID=your_google_client_id CLIENT_SECRET=your_google_client_secret GOOGLE_API_KEY=your_google_ai_api_key + JWT_SECRET=your_secure_jwt_secret ``` 2. **Build Docker images**: @@ -81,6 +82,7 @@ This setup is for actively developing the application with hot-reloading where p export CLIENT_ID=your_google_client_id export CLIENT_SECRET=your_google_client_secret export GOOGLE_API_KEY=your_google_ai_api_key + export JWT_SECRET=your_secure_jwt_secret sbt server/run ``` The server will be running on `http://localhost`. @@ -101,6 +103,7 @@ The application is configured using environment variables. | `CORS_URL` | The allowed origin for CORS requests. | `https://localhost` | No | | `GOOGLE_API_KEY` | The API key for Google's Generative AI. | - | For summary feature | | `JOB_TOKEN` | Secret token for triggering background jobs via HTTP. | - | No | +| `JWT_SECRET` | Secret string used for signing JWT tokens. | - | **Yes** | | `REGISTRY` | The Docker registry to push the image to | - | No | ## Deployment diff --git a/build.sbt b/build.sbt index 2eb3d30..d457069 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,11 @@ import com.typesafe.sbt.SbtNativePackager.autoImport.NativePackagerHelper.* import com.typesafe.sbt.packager.docker.* import org.scalajs.linker.interface.ModuleSplitStyle +import com.typesafe.sbt.packager.docker.DockerApiVersion import scala.sys.process.* lazy val projectVersion = "2.4.4-gcr" -lazy val gitCommitHash = Process("git rev-parse --short HEAD").!!.trim lazy val organizationName = "ru.trett" lazy val scala3Version = "3.7.4" lazy val circeVersion = "0.14.15" @@ -17,6 +17,7 @@ lazy val customScalaOptions = Seq("-Wunused:imports", "-rewrite", "-source:3.4-m lazy val buildClientDist = taskKey[File]("Build client optimized package") lazy val buildImage = taskKey[Unit]("Build docker image") lazy val pushImage = taskKey[Unit]("Push docker image to remote repository") +lazy val generateMigrationFiles = taskKey[Seq[File]]("Generate MigrationFiles.scala with list of SQL migrations") lazy val shared = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) @@ -68,14 +69,15 @@ lazy val client = project lazy val server = project .in(file("server")) .dependsOn(shared.jvm) - .enablePlugins(JavaAppPackaging, DockerPlugin) + .enablePlugins(JavaAppPackaging, DockerPlugin, GraalVMNativeImagePlugin) .settings( version := projectVersion, organization := organizationName, scalaVersion := scala3Version, name := "server", dockerPermissionStrategy := DockerPermissionStrategy.None, - dockerBaseImage := "gcr.io/distroless/java21", + dockerBaseImage := "debian:12-slim", + dockerApiVersion := Some(DockerApiVersion(1, 40)), dockerCommands := { val commands = dockerCommands.value val filteredCommands = commands.filter { @@ -89,14 +91,42 @@ lazy val server = project case _ => true } filteredCommands ++ Seq( + Cmd("RUN", "apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*"), Cmd("WORKDIR", "/opt/docker"), - ExecCmd("ENTRYPOINT", "java", "-cp", "/opt/docker/lib/*", "ru.trett.rss.server.Server") + ExecCmd( + "ENTRYPOINT", + "/opt/docker/bin/server" + ) ) }, - Docker / version := gitCommitHash, dockerRepository := sys.env.get("REGISTRY"), dockerExposedPorts := Seq(8080), + Docker / mappings := { + val nativeImage = (GraalVMNativeImage / packageBin).value + val standardMappings = (Docker / mappings).value + standardMappings.filter { case (file, path) => + !path.contains("bin/server") && !path.contains("lib/") + } :+ (nativeImage -> "/opt/docker/bin/server") + }, + graalVMNativeImageOptions ++= Seq( + "--no-fallback", + "-H:+ReportExceptionStackTraces", + "--verbose", + "--enable-https", + "--enable-http", + "-H:IncludeResources=application\\.conf", + "-H:IncludeResources=logback\\.xml", + "-H:IncludeResources=public/.*", + "-H:IncludeResources=db/migration/.*", + "-H:DeadlockWatchdogInterval=900", + "-Ob", + "-J-Xmx24G", + "-R:MaxHeapSize=512m", + "--initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback,com.fasterxml.jackson", + "--initialize-at-run-time=io.netty.channel.epoll.Epoll,io.netty.channel.epoll.Native,io.netty.channel.epoll.EpollEventLoop,io.netty.channel.epoll.EpollEventLoopGroup,io.netty.channel.kqueue.KQueue,io.netty.channel.kqueue.Native,io.netty.channel.kqueue.KQueueEventLoopGroup,org.http4s.MimeDB" + ), watchSources ++= (client / Compile / watchSources).value, + Compile / sourceGenerators += generateMigrationFiles.taskValue, libraryDependencies ++= Seq( "org.typelevel" %% "cats-effect" % "3.6.3", "org.slf4j" % "slf4j-api" % "2.0.17", @@ -126,7 +156,6 @@ lazy val server = project "org.tpolecat" %% "doobie-postgres-circe" ).map(_ % doobieVersion), libraryDependencies += "org.jsoup" % "jsoup" % "1.21.2", - libraryDependencies += "com.github.blemale" %% "scaffeine" % "5.3.0", libraryDependencies += "io.circe" %% "circe-fs2" % "0.14.1", libraryDependencies += "com.github.jwt-scala" %% "jwt-circe" % "10.0.1", libraryDependencies += "com.google.cloud.sql" % "postgres-socket-factory" % "1.15.1", @@ -140,7 +169,7 @@ lazy val server = project Compile / run / fork := true, Compile / packageDoc / mappings := Seq(), Compile / resourceGenerators += Def.task { - val _ = (client / Compile / fastLinkJS).value + val _ = (client / Compile / fullLinkJS).value val distDir = buildClientDist.value val targetDir = (Compile / resourceManaged).value / "public" IO.copyDirectory(distDir, targetDir) @@ -165,3 +194,34 @@ buildImage := { pushImage := { (server / Docker / publish).value } + +// Task to generate a Scala file containing a list of all Flyway migration files. +// This is required for GraalVM Native Image support because Flyway's classpath scanning +// doesn't work out-of-the-box in a native binary. +server / generateMigrationFiles := { + val resourceDir = (server / Compile / resourceDirectory).value + val migrationDir = resourceDir / "db" / "migration" + + // List all .sql files in the migration directory + val migrations = if (migrationDir.exists()) { + migrationDir.listFiles().filter(_.getName.endsWith(".sql")).map(_.getName).toList.sorted + } else Nil + + val file = + (server / Compile / sourceManaged).value / "ru" / "trett" / "rss" / "server" / "db" / "MigrationFiles.scala" + val migrationsStr = migrations.map(m => "\"" + m + "\"").mkString(", ") + val content = + s"""package ru.trett.rss.server.db + | + |/** + | * This file is automatically generated by sbt. + | * It contains a list of migration files to be used by Flyway at runtime. + | */ + |object MigrationFiles { + | val list = List($migrationsStr) + |} + |""".stripMargin + + IO.write(file, content) + Seq(file) +} diff --git a/client/images/android-chrome-192x192.png b/client/public/images/android-chrome-192x192.png similarity index 100% rename from client/images/android-chrome-192x192.png rename to client/public/images/android-chrome-192x192.png diff --git a/client/images/android-chrome-512x512.png b/client/public/images/android-chrome-512x512.png similarity index 100% rename from client/images/android-chrome-512x512.png rename to client/public/images/android-chrome-512x512.png diff --git a/client/images/apple-touch-icon.png b/client/public/images/apple-touch-icon.png similarity index 100% rename from client/images/apple-touch-icon.png rename to client/public/images/apple-touch-icon.png diff --git a/client/images/background.webp b/client/public/images/background.webp similarity index 100% rename from client/images/background.webp rename to client/public/images/background.webp diff --git a/client/images/favicon-16x16.png b/client/public/images/favicon-16x16.png similarity index 100% rename from client/images/favicon-16x16.png rename to client/public/images/favicon-16x16.png diff --git a/client/images/favicon-32x32.png b/client/public/images/favicon-32x32.png similarity index 100% rename from client/images/favicon-32x32.png rename to client/public/images/favicon-32x32.png diff --git a/client/images/favicon.ico b/client/public/images/favicon.ico similarity index 100% rename from client/images/favicon.ico rename to client/public/images/favicon.ico diff --git a/client/site.webmanifest b/client/public/site.webmanifest similarity index 100% rename from client/site.webmanifest rename to client/public/site.webmanifest diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..40e0583 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,35 @@ +steps: + - name: ghcr.io/graalvm/graalvm-ce:ol8-java17-22.3.3 + args: + - '-c' + - | + set -e + gu install native-image + + # Install nodejs, sbt and build dependencies + curl -fsSL https://rpm.nodesource.com/setup_20.x | bash - + microdnf install -y nodejs gcc zlib-devel + + # Install sbt manually since it's not in the base image + curl -L https://github.com/sbt/sbt/releases/download/v1.10.11/sbt-1.10.11.tgz | tar -xz -C /usr/local + ln -sf /usr/local/sbt/bin/sbt /usr/local/bin/sbt + + # Build the native image + # We use 'server/Docker/stage' which triggers the native image build and prepares the docker context + sbt "server/Docker/stage" + entrypoint: bash + - name: gcr.io/cloud-builders/docker + args: + - build + - '-t' + - $_AR_HOSTNAME/$_AR_PROJECT_ID/$_AR_REPOSITORY/rssreader:$SHORT_SHA + - server/target/docker/stage + - name: gcr.io/cloud-builders/docker + args: + - push + - $_AR_HOSTNAMEv/$_AR_PROJECT_ID/$_AR_REPOSITORY/rssreader:$SHORT_SHA +timeout: 1800s +options: + machineType: E2_HIGHCPU_32 + logging: CLOUD_LOGGING_ONLY + diff --git a/collector-config.yaml b/collector-config.yaml deleted file mode 100644 index 2f3f4bb..0000000 --- a/collector-config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -receivers: - otlp: - protocols: - grpc: - endpoint: "0.0.0.0:4317" - http: - endpoint: "0.0.0.0:4318" - -processors: - batch: - -exporters: - googlemanagedprometheus: - debug: - verbosity: normal - -service: - pipelines: - metrics: - receivers: [otlp] - processors: [batch] - exporters: [googlemanagedprometheus, debug] diff --git a/scripts/local-docker/docker-compose.yml b/scripts/local-docker/docker-compose.yml index aabab61..e1bda92 100644 --- a/scripts/local-docker/docker-compose.yml +++ b/scripts/local-docker/docker-compose.yml @@ -24,7 +24,7 @@ services: - host.docker.internal:host-gateway server: - image: server:9bb2749 + image: server:latest container_name: rss_server restart: always depends_on: @@ -38,3 +38,4 @@ services: CLIENT_ID: ${CLIENT_ID} CLIENT_SECRET: ${CLIENT_SECRET} GOOGLE_API_KEY: ${GOOGLE_API_KEY} + JWT_SECRET: ${JWT_SECRET:-dev-secret} diff --git a/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/jni-config.json b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/jni-config.json new file mode 100644 index 0000000..b44e20b --- /dev/null +++ b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/jni-config.json @@ -0,0 +1,48 @@ +[ +{ + "name":"[Lcom.sun.management.internal.DiagnosticCommandArgumentInfo;" +}, +{ + "name":"[Lcom.sun.management.internal.DiagnosticCommandInfo;" +}, +{ + "name":"com.sun.management.internal.DiagnosticCommandArgumentInfo", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","boolean","boolean","boolean","int"] }] +}, +{ + "name":"com.sun.management.internal.DiagnosticCommandInfo", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","boolean","java.util.List"] }] +}, +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.InternalError", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.util.Arrays", + "methods":[{"name":"asList","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"ru.trett.rss.server.Server", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/predefined-classes-config.json b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/predefined-classes-config.json new file mode 100644 index 0000000..0e79b2c --- /dev/null +++ b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/predefined-classes-config.json @@ -0,0 +1,8 @@ +[ + { + "type":"agent-extracted", + "classes":[ + ] + } +] + diff --git a/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/proxy-config.json b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/proxy-config.json new file mode 100644 index 0000000..4a9716c --- /dev/null +++ b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/proxy-config.json @@ -0,0 +1,5 @@ +[ + { + "interfaces":["sun.misc.SignalHandler"] + } +] diff --git a/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/reflect-config.json b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/reflect-config.json new file mode 100644 index 0000000..90474a5 --- /dev/null +++ b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/reflect-config.json @@ -0,0 +1,866 @@ +[ +{ + "name":"[B" +}, +{ + "name":"[C" +}, +{ + "name":"[D" +}, +{ + "name":"[F" +}, +{ + "name":"[I" +}, +{ + "name":"[J" +}, +{ + "name":"[Lcats.parse.Parser0;" +}, +{ + "name":"[Lcats.parse.Parser;" +}, +{ + "name":"[Ljava.lang.String;" +}, +{ + "name":"[Ljava.sql.Statement;" +}, +{ + "name":"[Ljavax.management.openmbean.CompositeData;" +}, +{ + "name":"[Lscala.util.Either;" +}, +{ + "name":"[S" +}, +{ + "name":"[Z" +}, +{ + "name":"cats.Later", + "fields":[{"name":"value$lzy1"}] +}, +{ + "name":"cats.effect.IO$", + "fields":[{"name":"secureRandom$lzy1"}] +}, +{ + "name":"cats.effect.std.Console$", + "fields":[{"name":"cats$effect$std$ConsoleCompanionPlatform$$stdinReader$lzy1"}] +}, +{ + "name":"cats.effect.std.Semaphore$impl", + "fields":[{"name":"Done$lzy1"}, {"name":"Wait$lzy1"}] +}, +{ + "name":"cats.effect.unsafe.Head", + "fields":[{"name":"head"}] +}, +{ + "name":"cats.effect.unsafe.Tail", + "fields":[{"name":"tailPublisher"}] +}, +{ + "name":"cats.effect.unsafe.metrics.ComputePoolSampler", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.ComputePoolSamplerMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.effect.unsafe.metrics.CpuStarvation", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.CpuStarvationMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.effect.unsafe.metrics.LiveFiberSnapshotTrigger", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.LiveFiberSnapshotTriggerMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.effect.unsafe.metrics.LocalQueueSampler", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.LocalQueueSamplerMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.effect.unsafe.metrics.PollerSampler", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.PollerSamplerMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.effect.unsafe.metrics.TimerHeapSampler", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.TimerHeapSamplerMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.effect.unsafe.metrics.WorkerThreadSampler", + "queryAllPublicConstructors":true +}, +{ + "name":"cats.effect.unsafe.metrics.WorkerThreadSamplerMBean", + "queryAllPublicMethods":true +}, +{ + "name":"cats.parse.Parser$State", + "fields":[{"name":"locationMap$lzy1"}] +}, +{ + "name":"cats.parse.Parser0", + "fields":[{"name":"hashCode$lzy1"}] +}, +{ + "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.util.DefaultJoranConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.ConsoleAppender", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.OutputStreamAppender", + "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] +}, +{ + "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA1", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA224", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA384", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA512", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.management.GarbageCollectorMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.GcInfo", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.HotSpotDiagnosticMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.ThreadMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.UnixOperatingSystemMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.VMOption", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.internal.GarbageCollectorExtImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.management.internal.HotSpotDiagnostic", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.management.internal.HotSpotThreadImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.management.internal.OperatingSystemImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.zaxxer.hikari.HikariConfig", + "allDeclaredFields":true +}, +{ + "name":"com.zaxxer.hikari.pool.PoolBase", + "fields":[{"name":"catalog"}] +}, +{ + "name":"com.zaxxer.hikari.pool.PoolEntry", + "fields":[{"name":"state"}] +}, +{ + "name":"doobie.free.KleisliInterpreter", + "fields":[{"name":"BlobInterpreter$lzy1"}, {"name":"CallableStatementInterpreter$lzy1"}, {"name":"ClobInterpreter$lzy1"}, {"name":"ConnectionInterpreter$lzy1"}, {"name":"DatabaseMetaDataInterpreter$lzy1"}, {"name":"DriverInterpreter$lzy1"}, {"name":"NClobInterpreter$lzy1"}, {"name":"PreparedStatementInterpreter$lzy1"}, {"name":"RefInterpreter$lzy1"}, {"name":"ResultSetInterpreter$lzy1"}, {"name":"SQLDataInterpreter$lzy1"}, {"name":"SQLInputInterpreter$lzy1"}, {"name":"SQLOutputInterpreter$lzy1"}, {"name":"StatementInterpreter$lzy1"}] +}, +{ + "name":"doobie.package$implicits$", + "fields":[{"name":"legacy$lzy1"}] +}, +{ + "name":"doobie.util.Write$Composite", + "fields":[{"name":"length$lzy1"}, {"name":"puts$lzy1"}] +}, +{ + "name":"doobie.util.fragment$Fragment", + "fields":[{"name":"write$lzy1"}] +}, +{ + "name":"doobie.util.meta.Meta$", + "fields":[{"name":"Advanced$lzy1"}, {"name":"Basic$lzy1"}] +}, +{ + "name":"doobie.util.meta.MetaConstructors$", + "fields":[{"name":"Advanced$lzy2"}, {"name":"Basic$lzy2"}] +}, +{ + "name":"fs2.Chunk$", + "fields":[{"name":"IArraySlice$lzy1"}] +}, +{ + "name":"fs2.internal.ScopedResource$$anon$1", + "fields":[{"name":"TheLease$lzy1"}] +}, +{ + "name":"fs2.io.net.NetworkCompanionPlatform$$anon$1", + "fields":[{"name":"fallback$lzy1"}] +}, +{ + "name":"java.lang.Boolean", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Byte", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Character", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.ClassValue" +}, +{ + "name":"java.lang.Comparable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Deprecated", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Double", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Float", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Integer", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Long", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.SecurityManager", + "methods":[{"name":"checkPermission","parameterTypes":["java.security.Permission"] }] +}, +{ + "name":"java.lang.Short", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.StackTraceElement", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.Thread", + "fields":[{"name":"threadLocalRandomProbe"}] +}, +{ + "name":"java.lang.Void", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.invoke.VarHandle", + "methods":[{"name":"releaseFence","parameterTypes":[] }] +}, +{ + "name":"java.lang.management.BufferPoolMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.ClassLoadingMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.CompilationMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.LockInfo", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.ManagementPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.management.MemoryMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MemoryManagerMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MemoryPoolMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MemoryUsage", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MonitorInfo", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.PlatformLoggingMXBean", + "queryAllPublicMethods":true, + "methods":[{"name":"getLoggerLevel","parameterTypes":["java.lang.String"] }, {"name":"getLoggerNames","parameterTypes":[] }, {"name":"getParentLoggerName","parameterTypes":["java.lang.String"] }, {"name":"setLoggerLevel","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.lang.management.RuntimeMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.ThreadInfo", + "queryAllPublicMethods":true +}, +{ + "name":"java.math.BigDecimal" +}, +{ + "name":"java.math.BigInteger" +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.Date" +}, +{ + "name":"java.util.PropertyPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.Striped64", + "fields":[{"name":"base"}, {"name":"cellsBusy"}] +}, +{ + "name":"java.util.logging.LogManager", + "methods":[{"name":"getLoggingMXBean","parameterTypes":[] }] +}, +{ + "name":"java.util.logging.LoggingMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"javax.management.MBeanOperationInfo", + "queryAllPublicMethods":true, + "methods":[{"name":"getSignature","parameterTypes":[] }] +}, +{ + "name":"javax.management.MBeanServerBuilder", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"javax.management.ObjectName" +}, +{ + "name":"javax.management.openmbean.CompositeData" +}, +{ + "name":"javax.management.openmbean.OpenMBeanOperationInfoSupport" +}, +{ + "name":"javax.management.openmbean.TabularData" +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"jdk.management.jfr.ConfigurationInfo", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.EventTypeInfo", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.FlightRecorderMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.RecordingInfo", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.SettingDescriptorInfo", + "queryAllPublicMethods":true +}, +{ + "name":"org.flywaydb.core.api.migration.baseline.BaselineMigrationConfigurationExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getBaselineMigrationPrefix","parameterTypes":[] }, {"name":"setBaselineMigrationPrefix","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"org.flywaydb.core.extensibility.ConfigurationExtension", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.flywaydb.core.extensibility.Plugin", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.flywaydb.core.internal.command.clean.CleanModeConfigurationExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getClean","parameterTypes":[] }, {"name":"setClean","parameterTypes":["org.flywaydb.core.internal.command.clean.CleanModel"] }] +}, +{ + "name":"org.flywaydb.core.internal.command.clean.CleanModel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.flywaydb.core.internal.command.clean.SchemaModel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.flywaydb.core.internal.logging.slf4j.Slf4jLogCreator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.flywaydb.core.internal.proprietaryStubs.LicensingConfigurationExtensionStub", + "allDeclaredFields":true +}, +{ + "name":"org.flywaydb.core.internal.proprietaryStubs.OfflinePermitConfigurationExtensionStub", + "allDeclaredFields":true +}, +{ + "name":"org.flywaydb.core.internal.proprietaryStubs.PATTokenConfigurationExtensionStub", + "allDeclaredFields":true +}, +{ + "name":"org.flywaydb.core.internal.publishing.PublishingConfigurationExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"isCheckDriftOnMigrate","parameterTypes":[] }, {"name":"isPublishResult","parameterTypes":[] }, {"name":"setCheckDriftOnMigrate","parameterTypes":["boolean"] }, {"name":"setPublishResult","parameterTypes":["boolean"] }] +}, +{ + "name":"org.flywaydb.database.postgresql.PostgreSQLConfigurationExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getTransactional","parameterTypes":[] }, {"name":"isTransactionalLock","parameterTypes":[] }, {"name":"setTransactional","parameterTypes":["org.flywaydb.database.postgresql.TransactionalModel"] }, {"name":"setTransactionalLock","parameterTypes":["boolean"] }] +}, +{ + "name":"org.flywaydb.database.postgresql.TransactionalModel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getLock","parameterTypes":[] }, {"name":"setLock","parameterTypes":["java.lang.Boolean"] }] +}, +{ + "name":"org.http4s.Header$Raw$", + "fields":[{"name":"catsInstancesForHttp4sHeaderRaw$lzy1"}] +}, +{ + "name":"org.http4s.Headers$", + "fields":[{"name":"HeadersOrder$lzy1"}] +}, +{ + "name":"org.http4s.MediaType$", + "fields":[{"name":"application$divgraphql$lzy1"}, {"name":"application$lzy1"}, {"name":"application_parts$lzy1"}, {"name":"audio$lzy1"}, {"name":"chemical$lzy1"}, {"name":"font$lzy1"}, {"name":"image$lzy1"}, {"name":"message$lzy1"}, {"name":"model$lzy1"}, {"name":"multipart$lzy1"}, {"name":"text$divevent$minusstream$lzy1"}, {"name":"text$lzy1"}, {"name":"video$lzy1"}, {"name":"x_conference$lzy1"}, {"name":"x_shader$lzy1"}] +}, +{ + "name":"org.http4s.MimeDB$application$", + "fields":[{"name":"1d$minusinterleaved$minusparityfec$lzy1"}, {"name":"3gpdash$minusqoe$minusreport$plusxml$lzy1"}, {"name":"3gpp$minusims$plusxml$lzy1"}, {"name":"3gpphal$plusjson$lzy1"}, {"name":"3gpphalforms$plusjson$lzy1"}, {"name":"a2l$lzy1"}, {"name":"ace$pluscbor$lzy1"}, {"name":"activemessage$lzy1"}, {"name":"activity$plusjson$lzy1"}, {"name":"alto$minuscostmap$plusjson$lzy1"}, {"name":"alto$minuscostmapfilter$plusjson$lzy1"}, {"name":"alto$minusdirectory$plusjson$lzy1"}, {"name":"alto$minusendpointcost$plusjson$lzy1"}, {"name":"alto$minusendpointcostparams$plusjson$lzy1"}, {"name":"alto$minusendpointprop$plusjson$lzy1"}, {"name":"alto$minusendpointpropparams$plusjson$lzy1"}, {"name":"alto$minuserror$plusjson$lzy1"}, {"name":"alto$minusnetworkmap$plusjson$lzy1"}, {"name":"alto$minusnetworkmapfilter$plusjson$lzy1"}, {"name":"alto$minusupdatestreamcontrol$plusjson$lzy1"}, {"name":"alto$minusupdatestreamparams$plusjson$lzy1"}, {"name":"aml$lzy1"}, {"name":"andrew$minusinset$lzy1"}, {"name":"applefile$lzy1"}, {"name":"applixware$lzy1"}, {"name":"at$plusjwt$lzy1"}, {"name":"atf$lzy1"}, {"name":"atfx$lzy1"}, {"name":"atom$plusxml$lzy1"}, {"name":"atomcat$plusxml$lzy1"}, {"name":"atomdeleted$plusxml$lzy1"}, {"name":"atomicmail$lzy1"}, {"name":"atomsvc$plusxml$lzy1"}, {"name":"atsc$minusdwd$plusxml$lzy1"}, {"name":"atsc$minusdynamic$minusevent$minusmessage$lzy1"}, {"name":"atsc$minusheld$plusxml$lzy1"}, {"name":"atsc$minusrdt$plusjson$lzy1"}, {"name":"atsc$minusrsat$plusxml$lzy1"}, {"name":"atxml$lzy1"}, {"name":"auth$minuspolicy$plusxml$lzy1"}, {"name":"bacnet$minusxdd$pluszip$lzy1"}, {"name":"batch$minussmtp$lzy1"}, {"name":"bdoc$lzy1"}, {"name":"beep$plusxml$lzy1"}, {"name":"calendar$plusjson$lzy1"}, {"name":"calendar$plusxml$lzy1"}, {"name":"call$minuscompletion$lzy1"}, {"name":"cals$minus1840$lzy1"}, {"name":"captive$plusjson$lzy1"}, {"name":"cbor$lzy1"}, {"name":"cbor$minusseq$lzy1"}, {"name":"cccex$lzy1"}, {"name":"ccmp$plusxml$lzy1"}, {"name":"ccxml$plusxml$lzy1"}, {"name":"cdfx$plusxml$lzy1"}, {"name":"cdmi$minuscapability$lzy1"}, {"name":"cdmi$minuscontainer$lzy1"}, {"name":"cdmi$minusdomain$lzy1"}, {"name":"cdmi$minusobject$lzy1"}, {"name":"cdmi$minusqueue$lzy1"}, {"name":"cdni$lzy1"}, {"name":"cea$lzy1"}, {"name":"cea$minus2018$plusxml$lzy1"}, {"name":"cellml$plusxml$lzy1"}, {"name":"cfw$lzy1"}, {"name":"city$plusjson$lzy1"}, {"name":"clr$lzy1"}, {"name":"clue$plusxml$lzy1"}, {"name":"clue_info$plusxml$lzy1"}, {"name":"cms$lzy1"}, {"name":"cnrp$plusxml$lzy1"}, {"name":"coap$minusgroup$plusjson$lzy1"}, {"name":"coap$minuspayload$lzy1"}, {"name":"commonground$lzy1"}, {"name":"conference$minusinfo$plusxml$lzy1"}, {"name":"cose$lzy1"}, {"name":"cose$minuskey$lzy1"}, {"name":"cose$minuskey$minusset$lzy1"}, {"name":"cpl$plusxml$lzy1"}, {"name":"csrattrs$lzy1"}, {"name":"csta$plusxml$lzy1"}, {"name":"cstadata$plusxml$lzy1"}, {"name":"csvm$plusjson$lzy1"}, {"name":"cu$minusseeme$lzy1"}, {"name":"cwt$lzy1"}, {"name":"cybercash$lzy1"}, {"name":"dart$lzy1"}, {"name":"dash$minuspatch$plusxml$lzy1"}, {"name":"dash$plusxml$lzy1"}, {"name":"dashdelta$lzy1"}, {"name":"davmount$plusxml$lzy1"}, {"name":"dca$minusrft$lzy1"}, {"name":"dcd$lzy1"}, {"name":"dec$minusdx$lzy1"}, {"name":"dialog$minusinfo$plusxml$lzy1"}, {"name":"dicom$lzy1"}, {"name":"dicom$plusjson$lzy1"}, {"name":"dicom$plusxml$lzy1"}, {"name":"dii$lzy1"}, {"name":"dit$lzy1"}, {"name":"dns$lzy1"}, {"name":"dns$minusmessage$lzy1"}, {"name":"dns$plusjson$lzy1"}, {"name":"docbook$plusxml$lzy1"}, {"name":"dots$pluscbor$lzy1"}, {"name":"dskpp$plusxml$lzy1"}, {"name":"dssc$plusder$lzy1"}, {"name":"dssc$plusxml$lzy1"}, {"name":"dvcs$lzy1"}, {"name":"ecmascript$lzy1"}, {"name":"edi$minusconsent$lzy1"}, {"name":"edi$minusx12$lzy1"}, {"name":"edifact$lzy1"}, {"name":"efi$lzy1"}, {"name":"elm$plusjson$lzy1"}, {"name":"elm$plusxml$lzy1"}, {"name":"emergencycalldata$u002Ecap$plusxml$lzy1"}, {"name":"emergencycalldata$u002Ecomment$plusxml$lzy1"}, {"name":"emergencycalldata$u002Econtrol$plusxml$lzy1"}, {"name":"emergencycalldata$u002Edeviceinfo$plusxml$lzy1"}, {"name":"emergencycalldata$u002Eecall$u002Emsd$lzy1"}, {"name":"emergencycalldata$u002Eproviderinfo$plusxml$lzy1"}, {"name":"emergencycalldata$u002Eserviceinfo$plusxml$lzy1"}, {"name":"emergencycalldata$u002Esubscriberinfo$plusxml$lzy1"}, {"name":"emergencycalldata$u002Eveds$plusxml$lzy1"}, {"name":"emma$plusxml$lzy1"}, {"name":"emotionml$plusxml$lzy1"}, {"name":"encaprtp$lzy1"}, {"name":"epp$plusxml$lzy1"}, {"name":"epub$pluszip$lzy1"}, {"name":"eshop$lzy1"}, {"name":"exi$lzy1"}, {"name":"expect$minusct$minusreport$plusjson$lzy1"}, {"name":"express$lzy1"}, {"name":"fastinfoset$lzy1"}, {"name":"fastsoap$lzy1"}, {"name":"fdt$plusxml$lzy1"}, {"name":"fhir$plusjson$lzy1"}, {"name":"fhir$plusxml$lzy1"}, {"name":"fido$u002Etrusted$minusapps$plusjson$lzy1"}, {"name":"fits$lzy1"}, {"name":"flexfec$lzy1"}, {"name":"font$minussfnt$lzy1"}, {"name":"font$minustdpfr$lzy1"}, {"name":"font$minuswoff$lzy1"}, {"name":"framework$minusattributes$plusxml$lzy1"}, {"name":"geo$plusjson$lzy1"}, {"name":"geo$plusjson$minusseq$lzy1"}, {"name":"geopackage$plussqlite3$lzy1"}, {"name":"geoxacml$plusxml$lzy1"}, {"name":"gltf$minusbuffer$lzy1"}, {"name":"gml$plusxml$lzy1"}, {"name":"gpx$plusxml$lzy1"}, {"name":"gxf$lzy1"}, {"name":"gzip$lzy1"}, {"name":"h224$lzy1"}, {"name":"held$plusxml$lzy1"}, {"name":"hjson$lzy1"}, {"name":"http$lzy1"}, {"name":"hyperstudio$lzy1"}, {"name":"ibe$minuskey$minusrequest$plusxml$lzy1"}, {"name":"ibe$minuspkg$minusreply$plusxml$lzy1"}, {"name":"ibe$minuspp$minusdata$lzy1"}, {"name":"iges$lzy1"}, {"name":"im$minusiscomposing$plusxml$lzy1"}, {"name":"index$lzy1"}, {"name":"index$u002Ecmd$lzy1"}, {"name":"index$u002Eobj$lzy1"}, {"name":"index$u002Eresponse$lzy1"}, {"name":"index$u002Evnd$lzy1"}, {"name":"inkml$plusxml$lzy1"}, {"name":"iotp$lzy1"}, {"name":"ipfix$lzy1"}, {"name":"ipp$lzy1"}, {"name":"isup$lzy1"}, {"name":"its$plusxml$lzy1"}, {"name":"java$minusarchive$lzy1"}, {"name":"java$minusserialized$minusobject$lzy1"}, {"name":"java$minusvm$lzy1"}, {"name":"javascript$lzy1"}, {"name":"jf2feed$plusjson$lzy1"}, {"name":"jose$lzy1"}, {"name":"jose$plusjson$lzy1"}, {"name":"jrd$plusjson$lzy1"}, {"name":"jscalendar$plusjson$lzy1"}, {"name":"json$lzy1"}, {"name":"json$minuspatch$plusjson$lzy1"}, {"name":"json$minusseq$lzy1"}, {"name":"json5$lzy1"}, {"name":"jsonml$plusjson$lzy1"}, {"name":"jwk$minusset$plusjson$lzy1"}, {"name":"jwk$plusjson$lzy1"}, {"name":"jwt$lzy1"}, {"name":"kpml$minusrequest$plusxml$lzy1"}, {"name":"kpml$minusresponse$plusxml$lzy1"}, {"name":"ld$plusjson$lzy1"}, {"name":"lgr$plusxml$lzy1"}, {"name":"link$minusformat$lzy1"}, {"name":"load$minuscontrol$plusxml$lzy1"}, {"name":"lost$plusxml$lzy1"}, {"name":"lostsync$plusxml$lzy1"}, {"name":"lpf$pluszip$lzy1"}, {"name":"lxf$lzy1"}, {"name":"mac$minusbinhex40$lzy1"}, {"name":"mac$minuscompactpro$lzy1"}, {"name":"macwriteii$lzy1"}, {"name":"mads$plusxml$lzy1"}, {"name":"manifest$plusjson$lzy1"}, {"name":"marc$lzy1"}, {"name":"marcxml$plusxml$lzy1"}, {"name":"mathematica$lzy1"}, {"name":"mathml$minuscontent$plusxml$lzy1"}, {"name":"mathml$minuspresentation$plusxml$lzy1"}, {"name":"mathml$plusxml$lzy1"}, {"name":"mbms$minusassociated$minusprocedure$minusdescription$plusxml$lzy1"}, {"name":"mbms$minusderegister$plusxml$lzy1"}, {"name":"mbms$minusenvelope$plusxml$lzy1"}, {"name":"mbms$minusmsk$minusresponse$plusxml$lzy1"}, {"name":"mbms$minusmsk$plusxml$lzy1"}, {"name":"mbms$minusprotection$minusdescription$plusxml$lzy1"}, {"name":"mbms$minusreception$minusreport$plusxml$lzy1"}, {"name":"mbms$minusregister$minusresponse$plusxml$lzy1"}, {"name":"mbms$minusregister$plusxml$lzy1"}, {"name":"mbms$minusschedule$plusxml$lzy1"}, {"name":"mbms$minususer$minusservice$minusdescription$plusxml$lzy1"}, {"name":"mbox$lzy1"}, {"name":"media$minuspolicy$minusdataset$plusxml$lzy1"}, {"name":"media_control$plusxml$lzy1"}, {"name":"mediaservercontrol$plusxml$lzy1"}, {"name":"merge$minuspatch$plusjson$lzy1"}, {"name":"metalink$plusxml$lzy1"}, {"name":"metalink4$plusxml$lzy1"}, {"name":"mets$plusxml$lzy1"}, {"name":"mf4$lzy1"}, {"name":"mikey$lzy1"}, {"name":"mipc$lzy1"}, {"name":"missing$minusblocks$pluscbor$minusseq$lzy1"}, {"name":"mmt$minusaei$plusxml$lzy1"}, {"name":"mmt$minususd$plusxml$lzy1"}, {"name":"mods$plusxml$lzy1"}, {"name":"moss$minuskeys$lzy1"}, {"name":"moss$minussignature$lzy1"}, {"name":"mosskey$minusdata$lzy1"}, {"name":"mosskey$minusrequest$lzy1"}, {"name":"mp21$lzy1"}, {"name":"mp4$lzy1"}, {"name":"mpeg4$minusgeneric$lzy1"}, {"name":"mpeg4$minusiod$lzy1"}, {"name":"mpeg4$minusiod$minusxmt$lzy1"}, {"name":"mrb$minusconsumer$plusxml$lzy1"}, {"name":"mrb$minuspublish$plusxml$lzy1"}, {"name":"msc$minusivr$plusxml$lzy1"}, {"name":"msc$minusmixer$plusxml$lzy1"}, {"name":"msword$lzy1"}, {"name":"mud$plusjson$lzy1"}, {"name":"multipart$minuscore$lzy1"}, {"name":"mxf$lzy1"}, {"name":"n$minusquads$lzy1"}, {"name":"n$minustriples$lzy1"}, {"name":"nasdata$lzy1"}, {"name":"news$minuscheckgroups$lzy1"}, {"name":"news$minusgroupinfo$lzy1"}, {"name":"news$minustransmission$lzy1"}, {"name":"nlsml$plusxml$lzy1"}, {"name":"node$lzy1"}, {"name":"nss$lzy1"}, {"name":"oauth$minusauthz$minusreq$plusjwt$lzy1"}, {"name":"oblivious$minusdns$minusmessage$lzy1"}, {"name":"ocsp$minusrequest$lzy1"}, {"name":"ocsp$minusresponse$lzy1"}, {"name":"octet$minusstream$lzy1"}, {"name":"oda$lzy1"}, {"name":"odm$plusxml$lzy1"}, {"name":"odx$lzy1"}, {"name":"oebps$minuspackage$plusxml$lzy1"}, {"name":"ogg$lzy1"}, {"name":"omdoc$plusxml$lzy1"}, {"name":"onenote$lzy1"}, {"name":"opc$minusnodeset$plusxml$lzy1"}, {"name":"oscore$lzy1"}, {"name":"oxps$lzy1"}, {"name":"p21$lzy1"}, {"name":"p21$pluszip$lzy1"}, {"name":"p2p$minusoverlay$plusxml$lzy1"}, {"name":"parityfec$lzy1"}, {"name":"passport$lzy1"}, {"name":"patch$minusops$minuserror$plusxml$lzy1"}, {"name":"pdf$lzy1"}, {"name":"pdx$lzy1"}, {"name":"pem$minuscertificate$minuschain$lzy1"}, {"name":"pgp$minusencrypted$lzy1"}, {"name":"pgp$minuskeys$lzy1"}, {"name":"pgp$minussignature$lzy1"}, {"name":"pics$minusrules$lzy1"}, {"name":"pidf$minusdiff$plusxml$lzy1"}, {"name":"pidf$plusxml$lzy1"}, {"name":"pkcs10$lzy1"}, {"name":"pkcs12$lzy1"}, {"name":"pkcs7$minusmime$lzy1"}, {"name":"pkcs7$minussignature$lzy1"}, {"name":"pkcs8$lzy1"}, {"name":"pkcs8$minusencrypted$lzy1"}, {"name":"pkix$minusattr$minuscert$lzy1"}, {"name":"pkix$minuscert$lzy1"}, {"name":"pkix$minuscrl$lzy1"}, {"name":"pkix$minuspkipath$lzy1"}, {"name":"pkixcmp$lzy1"}, {"name":"pls$plusxml$lzy1"}, {"name":"poc$minussettings$plusxml$lzy1"}, {"name":"postscript$lzy1"}, {"name":"ppsp$minustracker$plusjson$lzy1"}, {"name":"problem$plusjson$lzy1"}, {"name":"problem$plusxml$lzy1"}, {"name":"provenance$plusxml$lzy1"}, {"name":"prs$u002Ealvestrand$u002Etitrax$minussheet$lzy1"}, {"name":"prs$u002Ecww$lzy1"}, {"name":"prs$u002Ecyn$lzy1"}, {"name":"prs$u002Ehpub$pluszip$lzy1"}, {"name":"prs$u002Enprend$lzy1"}, {"name":"prs$u002Eplucker$lzy1"}, {"name":"prs$u002Erdf$minusxml$minuscrypt$lzy1"}, {"name":"prs$u002Exsf$plusxml$lzy1"}, {"name":"pskc$plusxml$lzy1"}, {"name":"pvd$plusjson$lzy1"}, {"name":"qsig$lzy1"}, {"name":"raml$plusyaml$lzy1"}, {"name":"raptorfec$lzy1"}, {"name":"rdap$plusjson$lzy1"}, {"name":"rdf$plusxml$lzy1"}, {"name":"reginfo$plusxml$lzy1"}, {"name":"relax$minusng$minuscompact$minussyntax$lzy1"}, {"name":"remote$minusprinting$lzy1"}, {"name":"reputon$plusjson$lzy1"}, {"name":"resource$minuslists$minusdiff$plusxml$lzy1"}, {"name":"resource$minuslists$plusxml$lzy1"}, {"name":"rfc$plusxml$lzy1"}, {"name":"riscos$lzy1"}, {"name":"rlmi$plusxml$lzy1"}, {"name":"rls$minusservices$plusxml$lzy1"}, {"name":"route$minusapd$plusxml$lzy1"}, {"name":"route$minuss$minustsid$plusxml$lzy1"}, {"name":"route$minususd$plusxml$lzy1"}, {"name":"rpki$minusghostbusters$lzy1"}, {"name":"rpki$minusmanifest$lzy1"}, {"name":"rpki$minuspublication$lzy1"}, {"name":"rpki$minusroa$lzy1"}, {"name":"rpki$minusupdown$lzy1"}, {"name":"rsd$plusxml$lzy1"}, {"name":"rss$plusxml$lzy1"}, {"name":"rtf$lzy1"}, {"name":"rtploopback$lzy1"}, {"name":"rtx$lzy1"}, {"name":"samlassertion$plusxml$lzy1"}, {"name":"samlmetadata$plusxml$lzy1"}, {"name":"sarif$minusexternal$minusproperties$plusjson$lzy1"}, {"name":"sarif$plusjson$lzy1"}, {"name":"sbe$lzy1"}, {"name":"sbml$plusxml$lzy1"}, {"name":"scaip$plusxml$lzy1"}, {"name":"scim$plusjson$lzy1"}, {"name":"scvp$minuscv$minusrequest$lzy1"}, {"name":"scvp$minuscv$minusresponse$lzy1"}, {"name":"scvp$minusvp$minusrequest$lzy1"}, {"name":"scvp$minusvp$minusresponse$lzy1"}, {"name":"sdp$lzy1"}, {"name":"secevent$plusjwt$lzy1"}, {"name":"senml$minusetch$pluscbor$lzy1"}, {"name":"senml$minusetch$plusjson$lzy1"}, {"name":"senml$minusexi$lzy1"}, {"name":"senml$pluscbor$lzy1"}, {"name":"senml$plusjson$lzy1"}, {"name":"senml$plusxml$lzy1"}, {"name":"sensml$minusexi$lzy1"}, {"name":"sensml$pluscbor$lzy1"}, {"name":"sensml$plusjson$lzy1"}, {"name":"sensml$plusxml$lzy1"}, {"name":"sep$minusexi$lzy1"}, {"name":"sep$plusxml$lzy1"}, {"name":"session$minusinfo$lzy1"}, {"name":"set$minuspayment$lzy1"}, {"name":"set$minuspayment$minusinitiation$lzy1"}, {"name":"set$minusregistration$lzy1"}, {"name":"set$minusregistration$minusinitiation$lzy1"}, {"name":"sgml$lzy1"}, {"name":"sgml$minusopen$minuscatalog$lzy1"}, {"name":"shf$plusxml$lzy1"}, {"name":"sieve$lzy1"}, {"name":"simple$minusfilter$plusxml$lzy1"}, {"name":"simple$minusmessage$minussummary$lzy1"}, {"name":"simplesymbolcontainer$lzy1"}, {"name":"sipc$lzy1"}, {"name":"slate$lzy1"}, {"name":"smil$lzy1"}, {"name":"smil$plusxml$lzy1"}, {"name":"smpte336m$lzy1"}, {"name":"soap$plusfastinfoset$lzy1"}, {"name":"soap$plusxml$lzy1"}, {"name":"sparql$minusquery$lzy1"}, {"name":"sparql$minusresults$plusxml$lzy1"}, {"name":"spdx$plusjson$lzy1"}, {"name":"spirits$minusevent$plusxml$lzy1"}, {"name":"sql$lzy1"}, {"name":"srgs$lzy1"}, {"name":"srgs$plusxml$lzy1"}, {"name":"sru$plusxml$lzy1"}, {"name":"ssdl$plusxml$lzy1"}, {"name":"ssml$plusxml$lzy1"}, {"name":"stix$plusjson$lzy1"}, {"name":"swid$plusxml$lzy1"}, {"name":"tamp$minusapex$minusupdate$lzy1"}, {"name":"tamp$minusapex$minusupdate$minusconfirm$lzy1"}, {"name":"tamp$minuscommunity$minusupdate$lzy1"}, {"name":"tamp$minuscommunity$minusupdate$minusconfirm$lzy1"}, {"name":"tamp$minuserror$lzy1"}, {"name":"tamp$minussequence$minusadjust$lzy1"}, {"name":"tamp$minussequence$minusadjust$minusconfirm$lzy1"}, {"name":"tamp$minusstatus$minusquery$lzy1"}, {"name":"tamp$minusstatus$minusresponse$lzy1"}, {"name":"tamp$minusupdate$lzy1"}, {"name":"tamp$minusupdate$minusconfirm$lzy1"}, {"name":"tar$lzy1"}, {"name":"taxii$plusjson$lzy1"}, {"name":"td$plusjson$lzy1"}, {"name":"tei$plusxml$lzy1"}, {"name":"tetra_isi$lzy1"}, {"name":"thraud$plusxml$lzy1"}, {"name":"timestamp$minusquery$lzy1"}, {"name":"timestamp$minusreply$lzy1"}, {"name":"timestamped$minusdata$lzy1"}, {"name":"tlsrpt$plusgzip$lzy1"}, {"name":"tlsrpt$plusjson$lzy1"}, {"name":"tnauthlist$lzy1"}, {"name":"token$minusintrospection$plusjwt$lzy1"}, {"name":"toml$lzy1"}, {"name":"trickle$minusice$minussdpfrag$lzy1"}, {"name":"trig$lzy1"}, {"name":"ttml$plusxml$lzy1"}, {"name":"tve$minustrigger$lzy1"}, {"name":"tzif$lzy1"}, {"name":"tzif$minusleap$lzy1"}, {"name":"ubjson$lzy1"}, {"name":"ulpfec$lzy1"}, {"name":"urc$minusgrpsheet$plusxml$lzy1"}, {"name":"urc$minusressheet$plusxml$lzy1"}, {"name":"urc$minustargetdesc$plusxml$lzy1"}, {"name":"urc$minusuisocketdesc$plusxml$lzy1"}, {"name":"vcard$plusjson$lzy1"}, {"name":"vcard$plusxml$lzy1"}, {"name":"vemmi$lzy1"}, {"name":"vividence$u002Escriptfile$lzy1"}, {"name":"vnd$u002E1000minds$u002Edecision$minusmodel$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$minusprose$minuspc3ch$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$minusprose$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$minusv2x$minuslocal$minusservice$minusinformation$lzy1"}, {"name":"vnd$u002E3gpp$u002E5gnas$lzy1"}, {"name":"vnd$u002E3gpp$u002Eaccess$minustransfer$minusevents$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Ebsf$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Egmop$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Egtpc$lzy1"}, {"name":"vnd$u002E3gpp$u002Einterworking$minusdata$lzy1"}, {"name":"vnd$u002E3gpp$u002Elpp$lzy1"}, {"name":"vnd$u002E3gpp$u002Emc$minussignalling$minusear$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minusaffiliation$minuscommand$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minuspayload$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minusservice$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minussignalling$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minusue$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcdata$minususer$minusprofile$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusaffiliation$minuscommand$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusfloor$minusrequest$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minuslocation$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusmbms$minususage$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusservice$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minussigned$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusue$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minusue$minusinit$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcptt$minususer$minusprofile$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minusaffiliation$minuscommand$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minusaffiliation$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minuslocation$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minusmbms$minususage$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minusservice$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minustransmission$minusrequest$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minusue$minusconfig$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emcvideo$minususer$minusprofile$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Emid$minuscall$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Engap$lzy1"}, {"name":"vnd$u002E3gpp$u002Epfcp$lzy1"}, {"name":"vnd$u002E3gpp$u002Epic$minusbw$minuslarge$lzy1"}, {"name":"vnd$u002E3gpp$u002Epic$minusbw$minussmall$lzy1"}, {"name":"vnd$u002E3gpp$u002Epic$minusbw$minusvar$lzy1"}, {"name":"vnd$u002E3gpp$u002Es1ap$lzy1"}, {"name":"vnd$u002E3gpp$u002Esms$lzy1"}, {"name":"vnd$u002E3gpp$u002Esms$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Esrvcc$minusext$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Esrvcc$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Estate$minusand$minusevent$minusinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp$u002Eussd$plusxml$lzy1"}, {"name":"vnd$u002E3gpp2$u002Ebcmcsinfo$plusxml$lzy1"}, {"name":"vnd$u002E3gpp2$u002Esms$lzy1"}, {"name":"vnd$u002E3gpp2$u002Etcap$lzy1"}, {"name":"vnd$u002E3lightssoftware$u002Eimagescal$lzy1"}, {"name":"vnd$u002E3m$u002Epost$minusit$minusnotes$lzy1"}, {"name":"vnd$u002Eaccpac$u002Esimply$u002Easo$lzy1"}, {"name":"vnd$u002Eaccpac$u002Esimply$u002Eimp$lzy1"}, {"name":"vnd$u002Eacucobol$lzy1"}, {"name":"vnd$u002Eacucorp$lzy1"}, {"name":"vnd$u002Eadobe$u002Eair$minusapplication$minusinstaller$minuspackage$pluszip$lzy1"}, {"name":"vnd$u002Eadobe$u002Eflash$u002Emovie$lzy1"}, {"name":"vnd$u002Eadobe$u002Eformscentral$u002Efcdt$lzy1"}, {"name":"vnd$u002Eadobe$u002Efxp$lzy1"}, {"name":"vnd$u002Eadobe$u002Epartial$minusupload$lzy1"}, {"name":"vnd$u002Eadobe$u002Exdp$plusxml$lzy1"}, {"name":"vnd$u002Eadobe$u002Exfdf$lzy1"}, {"name":"vnd$u002Eaether$u002Eimp$lzy1"}, {"name":"vnd$u002Eafpc$u002Eafplinedata$lzy1"}, {"name":"vnd$u002Eafpc$u002Eafplinedata$minuspagedef$lzy1"}, {"name":"vnd$u002Eafpc$u002Ecmoca$minuscmresource$lzy1"}, {"name":"vnd$u002Eafpc$u002Efoca$minuscharset$lzy1"}, {"name":"vnd$u002Eafpc$u002Efoca$minuscodedfont$lzy1"}, {"name":"vnd$u002Eafpc$u002Efoca$minuscodepage$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$minuscmtable$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$minusformdef$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$minusmediummap$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$minusobjectcontainer$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$minusoverlay$lzy1"}, {"name":"vnd$u002Eafpc$u002Emodca$minuspagesegment$lzy1"}, {"name":"vnd$u002Eage$lzy1"}, {"name":"vnd$u002Eah$minusbarcode$lzy1"}, {"name":"vnd$u002Eahead$u002Espace$lzy1"}, {"name":"vnd$u002Eairzip$u002Efilesecure$u002Eazf$lzy1"}, {"name":"vnd$u002Eairzip$u002Efilesecure$u002Eazs$lzy1"}, {"name":"vnd$u002Eamadeus$plusjson$lzy1"}, {"name":"vnd$u002Eamazon$u002Eebook$lzy1"}, {"name":"vnd$u002Eamazon$u002Emobi8$minusebook$lzy1"}, {"name":"vnd$u002Eamericandynamics$u002Eacc$lzy1"}, {"name":"vnd$u002Eamiga$u002Eami$lzy1"}, {"name":"vnd$u002Eamundsen$u002Emaze$plusxml$lzy1"}, {"name":"vnd$u002Eandroid$u002Eota$lzy1"}, {"name":"vnd$u002Eandroid$u002Epackage$minusarchive$lzy1"}, {"name":"vnd$u002Eanki$lzy1"}, {"name":"vnd$u002Eanser$minusweb$minuscertificate$minusissue$minusinitiation$lzy1"}, {"name":"vnd$u002Eanser$minusweb$minusfunds$minustransfer$minusinitiation$lzy1"}, {"name":"vnd$u002Eantix$u002Egame$minuscomponent$lzy1"}, {"name":"vnd$u002Eapache$u002Earrow$u002Efile$lzy1"}, {"name":"vnd$u002Eapache$u002Earrow$u002Estream$lzy1"}, {"name":"vnd$u002Eapache$u002Ethrift$u002Ebinary$lzy1"}, {"name":"vnd$u002Eapache$u002Ethrift$u002Ecompact$lzy1"}, {"name":"vnd$u002Eapache$u002Ethrift$u002Ejson$lzy1"}, {"name":"vnd$u002Eapi$plusjson$lzy1"}, {"name":"vnd$u002Eaplextor$u002Ewarrp$plusjson$lzy1"}, {"name":"vnd$u002Eapothekende$u002Ereservation$plusjson$lzy1"}, {"name":"vnd$u002Eapple$u002Einstaller$plusxml$lzy1"}, {"name":"vnd$u002Eapple$u002Ekeynote$lzy1"}, {"name":"vnd$u002Eapple$u002Empegurl$lzy1"}, {"name":"vnd$u002Eapple$u002Enumbers$lzy1"}, {"name":"vnd$u002Eapple$u002Epages$lzy1"}, {"name":"vnd$u002Eapple$u002Epkpass$lzy1"}, {"name":"vnd$u002Earastra$u002Eswi$lzy1"}, {"name":"vnd$u002Earistanetworks$u002Eswi$lzy1"}, {"name":"vnd$u002Eartisan$plusjson$lzy1"}, {"name":"vnd$u002Eartsquare$lzy1"}, {"name":"vnd$u002Eastraea$minussoftware$u002Eiota$lzy1"}, {"name":"vnd$u002Eaudiograph$lzy1"}, {"name":"vnd$u002Eautopackage$lzy1"}, {"name":"vnd$u002Eavalon$plusjson$lzy1"}, {"name":"vnd$u002Eavistar$plusxml$lzy1"}, {"name":"vnd$u002Ebalsamiq$u002Ebmml$plusxml$lzy1"}, {"name":"vnd$u002Ebalsamiq$u002Ebmpr$lzy1"}, {"name":"vnd$u002Ebanana$minusaccounting$lzy1"}, {"name":"vnd$u002Ebbf$u002Eusp$u002Eerror$lzy1"}, {"name":"vnd$u002Ebbf$u002Eusp$u002Emsg$lzy1"}, {"name":"vnd$u002Ebbf$u002Eusp$u002Emsg$plusjson$lzy1"}, {"name":"vnd$u002Ebekitzur$minusstech$plusjson$lzy1"}, {"name":"vnd$u002Ebint$u002Emed$minuscontent$lzy1"}, {"name":"vnd$u002Ebiopax$u002Erdf$plusxml$lzy1"}, {"name":"vnd$u002Eblink$minusidb$minusvalue$minuswrapper$lzy1"}, {"name":"vnd$u002Eblueice$u002Emultipass$lzy1"}, {"name":"vnd$u002Ebluetooth$u002Eep$u002Eoob$lzy1"}, {"name":"vnd$u002Ebluetooth$u002Ele$u002Eoob$lzy1"}, {"name":"vnd$u002Ebmi$lzy1"}, {"name":"vnd$u002Ebpf$lzy1"}, {"name":"vnd$u002Ebpf3$lzy1"}, {"name":"vnd$u002Ebusinessobjects$lzy1"}, {"name":"vnd$u002Ebyu$u002Euapi$plusjson$lzy1"}, {"name":"vnd$u002Ecab$minusjscript$lzy1"}, {"name":"vnd$u002Ecanon$minuscpdl$lzy1"}, {"name":"vnd$u002Ecanon$minuslips$lzy1"}, {"name":"vnd$u002Ecapasystems$minuspg$plusjson$lzy1"}, {"name":"vnd$u002Ecendio$u002Ethinlinc$u002Eclientconf$lzy1"}, {"name":"vnd$u002Ecentury$minussystems$u002Etcp_stream$lzy1"}, {"name":"vnd$u002Echemdraw$plusxml$lzy1"}, {"name":"vnd$u002Echess$minuspgn$lzy1"}, {"name":"vnd$u002Echipnuts$u002Ekaraoke$minusmmd$lzy1"}, {"name":"vnd$u002Eciedi$lzy1"}, {"name":"vnd$u002Ecinderella$lzy1"}, {"name":"vnd$u002Ecirpack$u002Eisdn$minusext$lzy1"}, {"name":"vnd$u002Ecitationstyles$u002Estyle$plusxml$lzy1"}, {"name":"vnd$u002Eclaymore$lzy1"}, {"name":"vnd$u002Ecloanto$u002Erp9$lzy1"}, {"name":"vnd$u002Eclonk$u002Ec4group$lzy1"}, {"name":"vnd$u002Ecluetrust$u002Ecartomobile$minusconfig$lzy1"}, {"name":"vnd$u002Ecluetrust$u002Ecartomobile$minusconfig$minuspkg$lzy1"}, {"name":"vnd$u002Ecoffeescript$lzy1"}, {"name":"vnd$u002Ecollabio$u002Exodocuments$u002Edocument$lzy1"}, {"name":"vnd$u002Ecollabio$u002Exodocuments$u002Edocument$minustemplate$lzy1"}, {"name":"vnd$u002Ecollabio$u002Exodocuments$u002Epresentation$lzy1"}, {"name":"vnd$u002Ecollabio$u002Exodocuments$u002Epresentation$minustemplate$lzy1"}, {"name":"vnd$u002Ecollabio$u002Exodocuments$u002Espreadsheet$lzy1"}, {"name":"vnd$u002Ecollabio$u002Exodocuments$u002Espreadsheet$minustemplate$lzy1"}, {"name":"vnd$u002Ecollection$plusjson$lzy1"}, {"name":"vnd$u002Ecollection$u002Edoc$plusjson$lzy1"}, {"name":"vnd$u002Ecollection$u002Enext$plusjson$lzy1"}, {"name":"vnd$u002Ecomicbook$minusrar$lzy1"}, {"name":"vnd$u002Ecomicbook$pluszip$lzy1"}, {"name":"vnd$u002Ecommerce$minusbattelle$lzy1"}, {"name":"vnd$u002Ecommonspace$lzy1"}, {"name":"vnd$u002Econtact$u002Ecmsg$lzy1"}, {"name":"vnd$u002Ecoreos$u002Eignition$plusjson$lzy1"}, {"name":"vnd$u002Ecosmocaller$lzy1"}, {"name":"vnd$u002Ecrick$u002Eclicker$lzy1"}, {"name":"vnd$u002Ecrick$u002Eclicker$u002Ekeyboard$lzy1"}, {"name":"vnd$u002Ecrick$u002Eclicker$u002Epalette$lzy1"}, {"name":"vnd$u002Ecrick$u002Eclicker$u002Etemplate$lzy1"}, {"name":"vnd$u002Ecrick$u002Eclicker$u002Ewordbank$lzy1"}, {"name":"vnd$u002Ecriticaltools$u002Ewbs$plusxml$lzy1"}, {"name":"vnd$u002Ecryptii$u002Epipe$plusjson$lzy1"}, {"name":"vnd$u002Ecrypto$minusshade$minusfile$lzy1"}, {"name":"vnd$u002Ecryptomator$u002Eencrypted$lzy1"}, {"name":"vnd$u002Ecryptomator$u002Evault$lzy1"}, {"name":"vnd$u002Ectc$minusposml$lzy1"}, {"name":"vnd$u002Ectct$u002Ews$plusxml$lzy1"}, {"name":"vnd$u002Ecups$minuspdf$lzy1"}, {"name":"vnd$u002Ecups$minuspostscript$lzy1"}, {"name":"vnd$u002Ecups$minusppd$lzy1"}, {"name":"vnd$u002Ecups$minusraster$lzy1"}, {"name":"vnd$u002Ecups$minusraw$lzy1"}, {"name":"vnd$u002Ecurl$lzy1"}, {"name":"vnd$u002Ecurl$u002Ecar$lzy1"}, {"name":"vnd$u002Ecurl$u002Epcurl$lzy1"}, {"name":"vnd$u002Ecyan$u002Edean$u002Eroot$plusxml$lzy1"}, {"name":"vnd$u002Ecybank$lzy1"}, {"name":"vnd$u002Ecyclonedx$plusjson$lzy1"}, {"name":"vnd$u002Ecyclonedx$plusxml$lzy1"}, {"name":"vnd$u002Ed2l$u002Ecoursepackage1p0$pluszip$lzy1"}, {"name":"vnd$u002Ed3m$minusdataset$lzy1"}, {"name":"vnd$u002Ed3m$minusproblem$lzy1"}, {"name":"vnd$u002Edart$lzy1"}, {"name":"vnd$u002Edata$minusvision$u002Erdz$lzy1"}, {"name":"vnd$u002Edatapackage$plusjson$lzy1"}, {"name":"vnd$u002Edataresource$plusjson$lzy1"}, {"name":"vnd$u002Edbf$lzy1"}, {"name":"vnd$u002Edebian$u002Ebinary$minuspackage$lzy1"}, {"name":"vnd$u002Edece$u002Edata$lzy1"}, {"name":"vnd$u002Edece$u002Ettml$plusxml$lzy1"}, {"name":"vnd$u002Edece$u002Eunspecified$lzy1"}, {"name":"vnd$u002Edece$u002Ezip$lzy1"}, {"name":"vnd$u002Edenovo$u002Efcselayout$minuslink$lzy1"}, {"name":"vnd$u002Edesmume$u002Emovie$lzy1"}, {"name":"vnd$u002Edir$minusbi$u002Eplate$minusdl$minusnosuffix$lzy1"}, {"name":"vnd$u002Edm$u002Edelegation$plusxml$lzy1"}, {"name":"vnd$u002Edna$lzy1"}, {"name":"vnd$u002Edocument$plusjson$lzy1"}, {"name":"vnd$u002Edolby$u002Emlp$lzy1"}, {"name":"vnd$u002Edolby$u002Emobile$u002E1$lzy1"}, {"name":"vnd$u002Edolby$u002Emobile$u002E2$lzy1"}, {"name":"vnd$u002Edoremir$u002Escorecloud$minusbinary$minusdocument$lzy1"}, {"name":"vnd$u002Edpgraph$lzy1"}, {"name":"vnd$u002Edreamfactory$lzy1"}, {"name":"vnd$u002Edrive$plusjson$lzy1"}, {"name":"vnd$u002Eds$minuskeypoint$lzy1"}, {"name":"vnd$u002Edtg$u002Elocal$lzy1"}, {"name":"vnd$u002Edtg$u002Elocal$u002Eflash$lzy1"}, {"name":"vnd$u002Edtg$u002Elocal$u002Ehtml$lzy1"}, {"name":"vnd$u002Edvb$u002Eait$lzy1"}, {"name":"vnd$u002Edvb$u002Edvbisl$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Edvbj$lzy1"}, {"name":"vnd$u002Edvb$u002Eesgcontainer$lzy1"}, {"name":"vnd$u002Edvb$u002Eipdcdftnotifaccess$lzy1"}, {"name":"vnd$u002Edvb$u002Eipdcesgaccess$lzy1"}, {"name":"vnd$u002Edvb$u002Eipdcesgaccess2$lzy1"}, {"name":"vnd$u002Edvb$u002Eipdcesgpdd$lzy1"}, {"name":"vnd$u002Edvb$u002Eipdcroaming$lzy1"}, {"name":"vnd$u002Edvb$u002Eiptv$u002Ealfec$minusbase$lzy1"}, {"name":"vnd$u002Edvb$u002Eiptv$u002Ealfec$minusenhancement$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minusaggregate$minusroot$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minuscontainer$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minusgeneric$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minusia$minusmsglist$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minusia$minusregistration$minusrequest$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minusia$minusregistration$minusresponse$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Enotif$minusinit$plusxml$lzy1"}, {"name":"vnd$u002Edvb$u002Epfr$lzy1"}, {"name":"vnd$u002Edvb$u002Eservice$lzy1"}, {"name":"vnd$u002Edxr$lzy1"}, {"name":"vnd$u002Edynageo$lzy1"}, {"name":"vnd$u002Edzr$lzy1"}, {"name":"vnd$u002Eeasykaraoke$u002Ecdgdownload$lzy1"}, {"name":"vnd$u002Eecdis$minusupdate$lzy1"}, {"name":"vnd$u002Eecip$u002Erlp$lzy1"}, {"name":"vnd$u002Eeclipse$u002Editto$plusjson$lzy1"}, {"name":"vnd$u002Eecowin$u002Echart$lzy1"}, {"name":"vnd$u002Eecowin$u002Efilerequest$lzy1"}, {"name":"vnd$u002Eecowin$u002Efileupdate$lzy1"}, {"name":"vnd$u002Eecowin$u002Eseries$lzy1"}, {"name":"vnd$u002Eecowin$u002Eseriesrequest$lzy1"}, {"name":"vnd$u002Eecowin$u002Eseriesupdate$lzy1"}, {"name":"vnd$u002Eefi$u002Eimg$lzy1"}, {"name":"vnd$u002Eefi$u002Eiso$lzy1"}, {"name":"vnd$u002Eemclient$u002Eaccessrequest$plusxml$lzy1"}, {"name":"vnd$u002Eenliven$lzy1"}, {"name":"vnd$u002Eenphase$u002Eenvoy$lzy1"}, {"name":"vnd$u002Eeprints$u002Edata$plusxml$lzy1"}, {"name":"vnd$u002Eepson$u002Eesf$lzy1"}, {"name":"vnd$u002Eepson$u002Emsf$lzy1"}, {"name":"vnd$u002Eepson$u002Equickanime$lzy1"}, {"name":"vnd$u002Eepson$u002Esalt$lzy1"}, {"name":"vnd$u002Eepson$u002Essf$lzy1"}, {"name":"vnd$u002Eericsson$u002Equickcall$lzy1"}, {"name":"vnd$u002Eespass$minusespass$pluszip$lzy1"}, {"name":"vnd$u002Eeszigno3$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eaoc$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Easic$minuse$pluszip$lzy1"}, {"name":"vnd$u002Eetsi$u002Easic$minuss$pluszip$lzy1"}, {"name":"vnd$u002Eetsi$u002Ecug$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvcommand$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvdiscovery$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvprofile$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvsad$minusbc$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvsad$minuscod$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvsad$minusnpvr$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvservice$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvsync$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Eiptvueprofile$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Emcid$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Emheg5$lzy1"}, {"name":"vnd$u002Eetsi$u002Eoverload$minuscontrol$minuspolicy$minusdataset$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Epstn$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Esci$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Esimservs$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Etimestamp$minustoken$lzy1"}, {"name":"vnd$u002Eetsi$u002Etsl$plusxml$lzy1"}, {"name":"vnd$u002Eetsi$u002Etsl$u002Eder$lzy1"}, {"name":"vnd$u002Eeu$u002Ekasparian$u002Ecar$plusjson$lzy1"}, {"name":"vnd$u002Eeudora$u002Edata$lzy1"}, {"name":"vnd$u002Eevolv$u002Eecig$u002Eprofile$lzy1"}, {"name":"vnd$u002Eevolv$u002Eecig$u002Esettings$lzy1"}, {"name":"vnd$u002Eevolv$u002Eecig$u002Etheme$lzy1"}, {"name":"vnd$u002Eexstream$minusempower$pluszip$lzy1"}, {"name":"vnd$u002Eexstream$minuspackage$lzy1"}, {"name":"vnd$u002Eezpix$minusalbum$lzy1"}, {"name":"vnd$u002Eezpix$minuspackage$lzy1"}, {"name":"vnd$u002Ef$minussecure$u002Emobile$lzy1"}, {"name":"vnd$u002Efamilysearch$u002Egedcom$pluszip$lzy1"}, {"name":"vnd$u002Efastcopy$minusdisk$minusimage$lzy1"}, {"name":"vnd$u002Efdf$lzy1"}, {"name":"vnd$u002Efdsn$u002Emseed$lzy1"}, {"name":"vnd$u002Efdsn$u002Eseed$lzy1"}, {"name":"vnd$u002Effsns$lzy1"}, {"name":"vnd$u002Eficlab$u002Eflb$pluszip$lzy1"}, {"name":"vnd$u002Efilmit$u002Ezfc$lzy1"}, {"name":"vnd$u002Efints$lzy1"}, {"name":"vnd$u002Efiremonkeys$u002Ecloudcell$lzy1"}, {"name":"vnd$u002Eflographit$lzy1"}, {"name":"vnd$u002Efluxtime$u002Eclip$lzy1"}, {"name":"vnd$u002Efont$minusfontforge$minussfd$lzy1"}, {"name":"vnd$u002Eframemaker$lzy1"}, {"name":"vnd$u002Efrogans$u002Efnc$lzy1"}, {"name":"vnd$u002Efrogans$u002Eltf$lzy1"}, {"name":"vnd$u002Efsc$u002Eweblaunch$lzy1"}, {"name":"vnd$u002Efujifilm$u002Efb$u002Edocuworks$lzy1"}, {"name":"vnd$u002Efujifilm$u002Efb$u002Edocuworks$u002Ebinder$lzy1"}, {"name":"vnd$u002Efujifilm$u002Efb$u002Edocuworks$u002Econtainer$lzy1"}, {"name":"vnd$u002Efujifilm$u002Efb$u002Ejfi$plusxml$lzy1"}, {"name":"vnd$u002Efujitsu$u002Eoasys$lzy1"}, {"name":"vnd$u002Efujitsu$u002Eoasys2$lzy1"}, {"name":"vnd$u002Efujitsu$u002Eoasys3$lzy1"}, {"name":"vnd$u002Efujitsu$u002Eoasysgp$lzy1"}, {"name":"vnd$u002Efujitsu$u002Eoasysprs$lzy1"}, {"name":"vnd$u002Efujixerox$u002Eart$minusex$lzy1"}, {"name":"vnd$u002Efujixerox$u002Eart4$lzy1"}, {"name":"vnd$u002Efujixerox$u002Eddd$lzy1"}, {"name":"vnd$u002Efujixerox$u002Edocuworks$lzy1"}, {"name":"vnd$u002Efujixerox$u002Edocuworks$u002Ebinder$lzy1"}, {"name":"vnd$u002Efujixerox$u002Edocuworks$u002Econtainer$lzy1"}, {"name":"vnd$u002Efujixerox$u002Ehbpl$lzy1"}, {"name":"vnd$u002Efut$minusmisnet$lzy1"}, {"name":"vnd$u002Efutoin$pluscbor$lzy1"}, {"name":"vnd$u002Efutoin$plusjson$lzy1"}, {"name":"vnd$u002Efuzzysheet$lzy1"}, {"name":"vnd$u002Egenomatix$u002Etuxedo$lzy1"}, {"name":"vnd$u002Egentics$u002Egrd$plusjson$lzy1"}, {"name":"vnd$u002Egeo$plusjson$lzy1"}, {"name":"vnd$u002Egeocube$plusxml$lzy1"}, {"name":"vnd$u002Egeogebra$u002Efile$lzy1"}, {"name":"vnd$u002Egeogebra$u002Eslides$lzy1"}, {"name":"vnd$u002Egeogebra$u002Etool$lzy1"}, {"name":"vnd$u002Egeometry$minusexplorer$lzy1"}, {"name":"vnd$u002Egeonext$lzy1"}, {"name":"vnd$u002Egeoplan$lzy1"}, {"name":"vnd$u002Egeospace$lzy1"}, {"name":"vnd$u002Egerber$lzy1"}, {"name":"vnd$u002Eglobalplatform$u002Ecard$minuscontent$minusmgt$lzy1"}, {"name":"vnd$u002Eglobalplatform$u002Ecard$minuscontent$minusmgt$minusresponse$lzy1"}, {"name":"vnd$u002Egmx$lzy1"}, {"name":"vnd$u002Egoogle$minusapps$u002Edocument$lzy1"}, {"name":"vnd$u002Egoogle$minusapps$u002Epresentation$lzy1"}, {"name":"vnd$u002Egoogle$minusapps$u002Espreadsheet$lzy1"}, {"name":"vnd$u002Egoogle$minusearth$u002Ekml$plusxml$lzy1"}, {"name":"vnd$u002Egoogle$minusearth$u002Ekmz$lzy1"}, {"name":"vnd$u002Egov$u002Esk$u002Ee$minusform$plusxml$lzy1"}, {"name":"vnd$u002Egov$u002Esk$u002Ee$minusform$pluszip$lzy1"}, {"name":"vnd$u002Egov$u002Esk$u002Exmldatacontainer$plusxml$lzy1"}, {"name":"vnd$u002Egrafeq$lzy1"}, {"name":"vnd$u002Egridmp$lzy1"}, {"name":"vnd$u002Egroove$minusaccount$lzy1"}, {"name":"vnd$u002Egroove$minushelp$lzy1"}, {"name":"vnd$u002Egroove$minusidentity$minusmessage$lzy1"}, {"name":"vnd$u002Egroove$minusinjector$lzy1"}, {"name":"vnd$u002Egroove$minustool$minusmessage$lzy1"}, {"name":"vnd$u002Egroove$minustool$minustemplate$lzy1"}, {"name":"vnd$u002Egroove$minusvcard$lzy1"}, {"name":"vnd$u002Ehal$plusjson$lzy1"}, {"name":"vnd$u002Ehal$plusxml$lzy1"}, {"name":"vnd$u002Ehandheld$minusentertainment$plusxml$lzy1"}, {"name":"vnd$u002Ehbci$lzy1"}, {"name":"vnd$u002Ehc$plusjson$lzy1"}, {"name":"vnd$u002Ehcl$minusbireports$lzy1"}, {"name":"vnd$u002Ehdt$lzy1"}, {"name":"vnd$u002Eheroku$plusjson$lzy1"}, {"name":"vnd$u002Ehhe$u002Elesson$minusplayer$lzy1"}, {"name":"vnd$u002Ehl7cda$plusxml$lzy1"}, {"name":"vnd$u002Ehl7v2$plusxml$lzy1"}, {"name":"vnd$u002Ehp$minushpgl$lzy1"}, {"name":"vnd$u002Ehp$minushpid$lzy1"}, {"name":"vnd$u002Ehp$minushps$lzy1"}, {"name":"vnd$u002Ehp$minusjlyt$lzy1"}, {"name":"vnd$u002Ehp$minuspcl$lzy1"}, {"name":"vnd$u002Ehp$minuspclxl$lzy1"}, {"name":"vnd$u002Ehttphone$lzy1"}, {"name":"vnd$u002Ehydrostatix$u002Esof$minusdata$lzy1"}, {"name":"vnd$u002Ehyper$minusitem$plusjson$lzy1"}, {"name":"vnd$u002Ehyper$plusjson$lzy1"}, {"name":"vnd$u002Ehyperdrive$plusjson$lzy1"}, {"name":"vnd$u002Ehzn$minus3d$minuscrossword$lzy1"}, {"name":"vnd$u002Eibm$u002Eafplinedata$lzy1"}, {"name":"vnd$u002Eibm$u002Eelectronic$minusmedia$lzy1"}, {"name":"vnd$u002Eibm$u002Eminipay$lzy1"}, {"name":"vnd$u002Eibm$u002Emodcap$lzy1"}, {"name":"vnd$u002Eibm$u002Erights$minusmanagement$lzy1"}, {"name":"vnd$u002Eibm$u002Esecure$minuscontainer$lzy1"}, {"name":"vnd$u002Eiccprofile$lzy1"}, {"name":"vnd$u002Eieee$u002E1905$lzy1"}, {"name":"vnd$u002Eigloader$lzy1"}, {"name":"vnd$u002Eimagemeter$u002Efolder$pluszip$lzy1"}, {"name":"vnd$u002Eimagemeter$u002Eimage$pluszip$lzy1"}, {"name":"vnd$u002Eimmervision$minusivp$lzy1"}, {"name":"vnd$u002Eimmervision$minusivu$lzy1"}, {"name":"vnd$u002Eims$u002Eimsccv1p1$lzy1"}, {"name":"vnd$u002Eims$u002Eimsccv1p2$lzy1"}, {"name":"vnd$u002Eims$u002Eimsccv1p3$lzy1"}, {"name":"vnd$u002Eims$u002Elis$u002Ev2$u002Eresult$plusjson$lzy1"}, {"name":"vnd$u002Eims$u002Elti$u002Ev2$u002Etoolconsumerprofile$plusjson$lzy1"}, {"name":"vnd$u002Eims$u002Elti$u002Ev2$u002Etoolproxy$plusjson$lzy1"}, {"name":"vnd$u002Eims$u002Elti$u002Ev2$u002Etoolproxy$u002Eid$plusjson$lzy1"}, {"name":"vnd$u002Eims$u002Elti$u002Ev2$u002Etoolsettings$plusjson$lzy1"}, {"name":"vnd$u002Eims$u002Elti$u002Ev2$u002Etoolsettings$u002Esimple$plusjson$lzy1"}, {"name":"vnd$u002Einformedcontrol$u002Erms$plusxml$lzy1"}, {"name":"vnd$u002Einformix$minusvisionary$lzy1"}, {"name":"vnd$u002Einfotech$u002Eproject$lzy1"}, {"name":"vnd$u002Einfotech$u002Eproject$plusxml$lzy1"}, {"name":"vnd$u002Einnopath$u002Ewamp$u002Enotification$lzy1"}, {"name":"vnd$u002Einsors$u002Eigm$lzy1"}, {"name":"vnd$u002Eintercon$u002Eformnet$lzy1"}, {"name":"vnd$u002Eintergeo$lzy1"}, {"name":"vnd$u002Eintertrust$u002Edigibox$lzy1"}, {"name":"vnd$u002Eintertrust$u002Enncp$lzy1"}, {"name":"vnd$u002Eintu$u002Eqbo$lzy1"}, {"name":"vnd$u002Eintu$u002Eqfx$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Ecatalogitem$plusxml$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Econceptitem$plusxml$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Eknowledgeitem$plusxml$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Enewsitem$plusxml$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Enewsmessage$plusxml$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Epackageitem$plusxml$lzy1"}, {"name":"vnd$u002Eiptc$u002Eg2$u002Eplanningitem$plusxml$lzy1"}, {"name":"vnd$u002Eipunplugged$u002Ercprofile$lzy1"}, {"name":"vnd$u002Eirepository$u002Epackage$plusxml$lzy1"}, {"name":"vnd$u002Eis$minusxpr$lzy1"}, {"name":"vnd$u002Eisac$u002Efcs$lzy1"}, {"name":"vnd$u002Eiso11783$minus10$pluszip$lzy1"}, {"name":"vnd$u002Ejam$lzy1"}, {"name":"vnd$u002Ejapannet$minusdirectory$minusservice$lzy1"}, {"name":"vnd$u002Ejapannet$minusjpnstore$minuswakeup$lzy1"}, {"name":"vnd$u002Ejapannet$minuspayment$minuswakeup$lzy1"}, {"name":"vnd$u002Ejapannet$minusregistration$lzy1"}, {"name":"vnd$u002Ejapannet$minusregistration$minuswakeup$lzy1"}, {"name":"vnd$u002Ejapannet$minussetstore$minuswakeup$lzy1"}, {"name":"vnd$u002Ejapannet$minusverification$lzy1"}, {"name":"vnd$u002Ejapannet$minusverification$minuswakeup$lzy1"}, {"name":"vnd$u002Ejcp$u002Ejavame$u002Emidlet$minusrms$lzy1"}, {"name":"vnd$u002Ejisp$lzy1"}, {"name":"vnd$u002Ejoost$u002Ejoda$minusarchive$lzy1"}, {"name":"vnd$u002Ejsk$u002Eisdn$minusngn$lzy1"}, {"name":"vnd$u002Ekahootz$lzy1"}, {"name":"vnd$u002Ekde$u002Ekarbon$lzy1"}, {"name":"vnd$u002Ekde$u002Ekchart$lzy1"}, {"name":"vnd$u002Ekde$u002Ekformula$lzy1"}, {"name":"vnd$u002Ekde$u002Ekivio$lzy1"}, {"name":"vnd$u002Ekde$u002Ekontour$lzy1"}, {"name":"vnd$u002Ekde$u002Ekpresenter$lzy1"}, {"name":"vnd$u002Ekde$u002Ekspread$lzy1"}, {"name":"vnd$u002Ekde$u002Ekword$lzy1"}, {"name":"vnd$u002Ekenameaapp$lzy1"}, {"name":"vnd$u002Ekidspiration$lzy1"}, {"name":"vnd$u002Ekinar$lzy1"}, {"name":"vnd$u002Ekoan$lzy1"}, {"name":"vnd$u002Ekodak$minusdescriptor$lzy1"}, {"name":"vnd$u002Elas$lzy1"}, {"name":"vnd$u002Elas$u002Elas$plusjson$lzy1"}, {"name":"vnd$u002Elas$u002Elas$plusxml$lzy1"}, {"name":"vnd$u002Elaszip$lzy1"}, {"name":"vnd$u002Eleap$plusjson$lzy1"}, {"name":"vnd$u002Eliberty$minusrequest$plusxml$lzy1"}, {"name":"vnd$u002Ellamagraphics$u002Elife$minusbalance$u002Edesktop$lzy1"}, {"name":"vnd$u002Ellamagraphics$u002Elife$minusbalance$u002Eexchange$plusxml$lzy1"}, {"name":"vnd$u002Elogipipe$u002Ecircuit$pluszip$lzy1"}, {"name":"vnd$u002Eloom$lzy1"}, {"name":"vnd$u002Elotus$minus1$minus2$minus3$lzy1"}, {"name":"vnd$u002Elotus$minusapproach$lzy1"}, {"name":"vnd$u002Elotus$minusfreelance$lzy1"}, {"name":"vnd$u002Elotus$minusnotes$lzy1"}, {"name":"vnd$u002Elotus$minusorganizer$lzy1"}, {"name":"vnd$u002Elotus$minusscreencam$lzy1"}, {"name":"vnd$u002Elotus$minuswordpro$lzy1"}, {"name":"vnd$u002Emacports$u002Eportpkg$lzy1"}, {"name":"vnd$u002Emapbox$minusvector$minustile$lzy1"}, {"name":"vnd$u002Emarlin$u002Edrm$u002Eactiontoken$plusxml$lzy1"}, {"name":"vnd$u002Emarlin$u002Edrm$u002Econftoken$plusxml$lzy1"}, {"name":"vnd$u002Emarlin$u002Edrm$u002Elicense$plusxml$lzy1"}, {"name":"vnd$u002Emarlin$u002Edrm$u002Emdcf$lzy1"}, {"name":"vnd$u002Emason$plusjson$lzy1"}, {"name":"vnd$u002Emaxar$u002Earchive$u002E3tz$pluszip$lzy1"}, {"name":"vnd$u002Emaxmind$u002Emaxmind$minusdb$lzy1"}, {"name":"vnd$u002Emcd$lzy1"}, {"name":"vnd$u002Emedcalcdata$lzy1"}, {"name":"vnd$u002Emediastation$u002Ecdkey$lzy1"}, {"name":"vnd$u002Emeridian$minusslingshot$lzy1"}, {"name":"vnd$u002Emfer$lzy1"}, {"name":"vnd$u002Emfmp$lzy1"}, {"name":"vnd$u002Emicro$plusjson$lzy1"}, {"name":"vnd$u002Emicrografx$u002Eflo$lzy1"}, {"name":"vnd$u002Emicrografx$u002Eigx$lzy1"}, {"name":"vnd$u002Emicrosoft$u002Eportable$minusexecutable$lzy1"}, {"name":"vnd$u002Emicrosoft$u002Ewindows$u002Ethumbnail$minuscache$lzy1"}, {"name":"vnd$u002Emiele$plusjson$lzy1"}, {"name":"vnd$u002Emif$lzy1"}, {"name":"vnd$u002Eminisoft$minushp3000$minussave$lzy1"}, {"name":"vnd$u002Emitsubishi$u002Emisty$minusguard$u002Etrustweb$lzy1"}, {"name":"vnd$u002Emobius$u002Edaf$lzy1"}, {"name":"vnd$u002Emobius$u002Edis$lzy1"}, {"name":"vnd$u002Emobius$u002Embk$lzy1"}, {"name":"vnd$u002Emobius$u002Emqy$lzy1"}, {"name":"vnd$u002Emobius$u002Emsl$lzy1"}, {"name":"vnd$u002Emobius$u002Eplc$lzy1"}, {"name":"vnd$u002Emobius$u002Etxf$lzy1"}, {"name":"vnd$u002Emophun$u002Eapplication$lzy1"}, {"name":"vnd$u002Emophun$u002Ecertificate$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$u002Eadsi$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$u002Efis$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$u002Egotap$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$u002Ekmr$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$u002Ettc$lzy1"}, {"name":"vnd$u002Emotorola$u002Eflexsuite$u002Ewem$lzy1"}, {"name":"vnd$u002Emotorola$u002Eiprm$lzy1"}, {"name":"vnd$u002Emozilla$u002Exul$plusxml$lzy1"}, {"name":"vnd$u002Ems$minus3mfdocument$lzy1"}, {"name":"vnd$u002Ems$minusartgalry$lzy1"}, {"name":"vnd$u002Ems$minusasf$lzy1"}, {"name":"vnd$u002Ems$minuscab$minuscompressed$lzy1"}, {"name":"vnd$u002Ems$minuscolor$u002Eiccprofile$lzy1"}, {"name":"vnd$u002Ems$minusexcel$lzy1"}, {"name":"vnd$u002Ems$minusexcel$u002Eaddin$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusexcel$u002Esheet$u002Ebinary$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusexcel$u002Esheet$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusexcel$u002Etemplate$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusfontobject$lzy1"}, {"name":"vnd$u002Ems$minushtmlhelp$lzy1"}, {"name":"vnd$u002Ems$minusims$lzy1"}, {"name":"vnd$u002Ems$minuslrm$lzy1"}, {"name":"vnd$u002Ems$minusoffice$u002Eactivex$plusxml$lzy1"}, {"name":"vnd$u002Ems$minusofficetheme$lzy1"}, {"name":"vnd$u002Ems$minusopentype$lzy1"}, {"name":"vnd$u002Ems$minusoutlook$lzy1"}, {"name":"vnd$u002Ems$minuspackage$u002Eobfuscated$minusopentype$lzy1"}, {"name":"vnd$u002Ems$minuspki$u002Eseccat$lzy1"}, {"name":"vnd$u002Ems$minuspki$u002Estl$lzy1"}, {"name":"vnd$u002Ems$minusplayready$u002Einitiator$plusxml$lzy1"}, {"name":"vnd$u002Ems$minuspowerpoint$lzy1"}, {"name":"vnd$u002Ems$minuspowerpoint$u002Eaddin$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minuspowerpoint$u002Epresentation$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minuspowerpoint$u002Eslide$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minuspowerpoint$u002Eslideshow$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minuspowerpoint$u002Etemplate$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusprintdevicecapabilities$plusxml$lzy1"}, {"name":"vnd$u002Ems$minusprinting$u002Eprintticket$plusxml$lzy1"}, {"name":"vnd$u002Ems$minusprintschematicket$plusxml$lzy1"}, {"name":"vnd$u002Ems$minusproject$lzy1"}, {"name":"vnd$u002Ems$minustnef$lzy1"}, {"name":"vnd$u002Ems$minuswindows$u002Edevicepairing$lzy1"}, {"name":"vnd$u002Ems$minuswindows$u002Enwprinting$u002Eoob$lzy1"}, {"name":"vnd$u002Ems$minuswindows$u002Eprinterpairing$lzy1"}, {"name":"vnd$u002Ems$minuswindows$u002Ewsd$u002Eoob$lzy1"}, {"name":"vnd$u002Ems$minuswmdrm$u002Elic$minuschlg$minusreq$lzy1"}, {"name":"vnd$u002Ems$minuswmdrm$u002Elic$minusresp$lzy1"}, {"name":"vnd$u002Ems$minuswmdrm$u002Emeter$minuschlg$minusreq$lzy1"}, {"name":"vnd$u002Ems$minuswmdrm$u002Emeter$minusresp$lzy1"}, {"name":"vnd$u002Ems$minusword$u002Edocument$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusword$u002Etemplate$u002Emacroenabled$u002E12$lzy1"}, {"name":"vnd$u002Ems$minusworks$lzy1"}, {"name":"vnd$u002Ems$minuswpl$lzy1"}, {"name":"vnd$u002Ems$minusxpsdocument$lzy1"}, {"name":"vnd$u002Emsa$minusdisk$minusimage$lzy1"}, {"name":"vnd$u002Emseq$lzy1"}, {"name":"vnd$u002Emsign$lzy1"}, {"name":"vnd$u002Emultiad$u002Ecreator$lzy1"}, {"name":"vnd$u002Emultiad$u002Ecreator$u002Ecif$lzy1"}, {"name":"vnd$u002Emusic$minusniff$lzy1"}, {"name":"vnd$u002Emusician$lzy1"}, {"name":"vnd$u002Emuvee$u002Estyle$lzy1"}, {"name":"vnd$u002Emynfc$lzy1"}, {"name":"vnd$u002Enacamar$u002Eybrid$plusjson$lzy1"}, {"name":"vnd$u002Encd$u002Econtrol$lzy1"}, {"name":"vnd$u002Encd$u002Ereference$lzy1"}, {"name":"vnd$u002Enearst$u002Einv$plusjson$lzy1"}, {"name":"vnd$u002Enebumind$u002Eline$lzy1"}, {"name":"vnd$u002Enervana$lzy1"}, {"name":"vnd$u002Enetfpx$lzy1"}, {"name":"vnd$u002Eneurolanguage$u002Enlu$lzy1"}, {"name":"vnd$u002Enimn$lzy1"}, {"name":"vnd$u002Enintendo$u002Enitro$u002Erom$lzy1"}, {"name":"vnd$u002Enintendo$u002Esnes$u002Erom$lzy1"}, {"name":"vnd$u002Enitf$lzy1"}, {"name":"vnd$u002Enoblenet$minusdirectory$lzy1"}, {"name":"vnd$u002Enoblenet$minussealer$lzy1"}, {"name":"vnd$u002Enoblenet$minusweb$lzy1"}, {"name":"vnd$u002Enokia$u002Ecatalogs$lzy1"}, {"name":"vnd$u002Enokia$u002Econml$pluswbxml$lzy1"}, {"name":"vnd$u002Enokia$u002Econml$plusxml$lzy1"}, {"name":"vnd$u002Enokia$u002Eiptv$u002Econfig$plusxml$lzy1"}, {"name":"vnd$u002Enokia$u002Eisds$minusradio$minuspresets$lzy1"}, {"name":"vnd$u002Enokia$u002Elandmark$pluswbxml$lzy1"}, {"name":"vnd$u002Enokia$u002Elandmark$plusxml$lzy1"}, {"name":"vnd$u002Enokia$u002Elandmarkcollection$plusxml$lzy1"}, {"name":"vnd$u002Enokia$u002En$minusgage$u002Eac$plusxml$lzy1"}, {"name":"vnd$u002Enokia$u002En$minusgage$u002Edata$lzy1"}, {"name":"vnd$u002Enokia$u002En$minusgage$u002Esymbian$u002Einstall$lzy1"}, {"name":"vnd$u002Enokia$u002Encd$lzy1"}, {"name":"vnd$u002Enokia$u002Epcd$pluswbxml$lzy1"}, {"name":"vnd$u002Enokia$u002Epcd$plusxml$lzy1"}, {"name":"vnd$u002Enokia$u002Eradio$minuspreset$lzy1"}, {"name":"vnd$u002Enokia$u002Eradio$minuspresets$lzy1"}, {"name":"vnd$u002Enovadigm$u002Eedm$lzy1"}, {"name":"vnd$u002Enovadigm$u002Eedx$lzy1"}, {"name":"vnd$u002Enovadigm$u002Eext$lzy1"}, {"name":"vnd$u002Entt$minuslocal$u002Econtent$minusshare$lzy1"}, {"name":"vnd$u002Entt$minuslocal$u002Efile$minustransfer$lzy1"}, {"name":"vnd$u002Entt$minuslocal$u002Eogw_remote$minusaccess$lzy1"}, {"name":"vnd$u002Entt$minuslocal$u002Esip$minusta_remote$lzy1"}, {"name":"vnd$u002Entt$minuslocal$u002Esip$minusta_tcp_stream$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Echart$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Echart$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Edatabase$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Eformula$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Eformula$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Egraphics$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Egraphics$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Eimage$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Eimage$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Epresentation$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Epresentation$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Espreadsheet$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Espreadsheet$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Etext$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Etext$minusmaster$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Etext$minustemplate$lzy1"}, {"name":"vnd$u002Eoasis$u002Eopendocument$u002Etext$minusweb$lzy1"}, {"name":"vnd$u002Eobn$lzy1"}, {"name":"vnd$u002Eocf$pluscbor$lzy1"}, {"name":"vnd$u002Eoci$u002Eimage$u002Emanifest$u002Ev1$plusjson$lzy1"}, {"name":"vnd$u002Eoftn$u002El10n$plusjson$lzy1"}, {"name":"vnd$u002Eoipf$u002Econtentaccessdownload$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Econtentaccessstreaming$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Ecspg$minushexbinary$lzy1"}, {"name":"vnd$u002Eoipf$u002Edae$u002Esvg$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Edae$u002Exhtml$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Emippvcontrolmessage$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Epae$u002Egem$lzy1"}, {"name":"vnd$u002Eoipf$u002Espdiscovery$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Espdlist$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Eueprofile$plusxml$lzy1"}, {"name":"vnd$u002Eoipf$u002Euserprofile$plusxml$lzy1"}, {"name":"vnd$u002Eolpc$minussugar$lzy1"}, {"name":"vnd$u002Eoma$minusscws$minusconfig$lzy1"}, {"name":"vnd$u002Eoma$minusscws$minushttp$minusrequest$lzy1"}, {"name":"vnd$u002Eoma$minusscws$minushttp$minusresponse$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Eassociated$minusprocedure$minusparameter$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Edrm$minustrigger$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Eimd$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Eltkm$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Enotification$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Eprovisioningtrigger$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Esgboot$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Esgdd$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Esgdu$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Esimple$minussymbol$minuscontainer$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Esmartcard$minustrigger$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Esprov$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ebcast$u002Estkm$lzy1"}, {"name":"vnd$u002Eoma$u002Ecab$minusaddress$minusbook$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ecab$minusfeature$minushandler$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ecab$minuspcc$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ecab$minussubs$minusinvite$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Ecab$minususer$minusprefs$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Edcd$lzy1"}, {"name":"vnd$u002Eoma$u002Edcdc$lzy1"}, {"name":"vnd$u002Eoma$u002Edd2$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Edrm$u002Erisd$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Egroup$minususage$minuslist$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Elwm2m$pluscbor$lzy1"}, {"name":"vnd$u002Eoma$u002Elwm2m$plusjson$lzy1"}, {"name":"vnd$u002Eoma$u002Elwm2m$plustlv$lzy1"}, {"name":"vnd$u002Eoma$u002Epal$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Epoc$u002Edetailed$minusprogress$minusreport$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Epoc$u002Efinal$minusreport$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Epoc$u002Egroups$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Epoc$u002Einvocation$minusdescriptor$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Epoc$u002Eoptimized$minusprogress$minusreport$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Epush$lzy1"}, {"name":"vnd$u002Eoma$u002Escidm$u002Emessages$plusxml$lzy1"}, {"name":"vnd$u002Eoma$u002Excap$minusdirectory$plusxml$lzy1"}, {"name":"vnd$u002Eomads$minusemail$plusxml$lzy1"}, {"name":"vnd$u002Eomads$minusfile$plusxml$lzy1"}, {"name":"vnd$u002Eomads$minusfolder$plusxml$lzy1"}, {"name":"vnd$u002Eomaloc$minussupl$minusinit$lzy1"}, {"name":"vnd$u002Eonepager$lzy1"}, {"name":"vnd$u002Eonepagertamp$lzy1"}, {"name":"vnd$u002Eonepagertamx$lzy1"}, {"name":"vnd$u002Eonepagertat$lzy1"}, {"name":"vnd$u002Eonepagertatp$lzy1"}, {"name":"vnd$u002Eonepagertatx$lzy1"}, {"name":"vnd$u002Eopenblox$u002Egame$minusbinary$lzy1"}, {"name":"vnd$u002Eopenblox$u002Egame$plusxml$lzy1"}, {"name":"vnd$u002Eopeneye$u002Eoeb$lzy1"}, {"name":"vnd$u002Eopenofficeorg$u002Eextension$lzy1"}, {"name":"vnd$u002Eopenstreetmap$u002Edata$plusxml$lzy1"}, {"name":"vnd$u002Eopentimestamps$u002Eots$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ecustom$minusproperties$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ecustomxmlproperties$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawing$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawingml$u002Echart$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawingml$u002Echartshapes$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawingml$u002Ediagramcolors$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawingml$u002Ediagramdata$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawingml$u002Ediagramlayout$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Edrawingml$u002Ediagramstyle$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Eextended$minusproperties$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Ecommentauthors$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Ecomments$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Ehandoutmaster$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Enotesmaster$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Enotesslide$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Epresentation$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Epresentation$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Epresprops$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslide$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslide$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslidelayout$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslidemaster$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslideshow$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslideshow$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslideupdateinfo$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Etablestyles$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Etags$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Etemplate$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Etemplate$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eviewprops$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Ecalcchain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Echartsheet$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Ecomments$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Econnections$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Edialogsheet$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Eexternallink$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Epivotcachedefinition$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Epivotcacherecords$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Epivottable$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Equerytable$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Erevisionheaders$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Erevisionlog$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Esharedstrings$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Esheet$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Esheet$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Esheetmetadata$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Estyles$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Etable$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Etablesinglecells$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Etemplate$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Etemplate$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Eusernames$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Evolatiledependencies$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Eworksheet$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Etheme$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ethemeoverride$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Evmldrawing$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Ecomments$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Edocument$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Edocument$u002Eglossary$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Edocument$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Eendnotes$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Efonttable$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Efooter$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Efootnotes$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Enumbering$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Esettings$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Estyles$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Etemplate$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Etemplate$u002Emain$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Ewebsettings$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minuspackage$u002Ecore$minusproperties$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minuspackage$u002Edigital$minussignature$minusxmlsignature$plusxml$lzy1"}, {"name":"vnd$u002Eopenxmlformats$minuspackage$u002Erelationships$plusxml$lzy1"}, {"name":"vnd$u002Eoracle$u002Eresource$plusjson$lzy1"}, {"name":"vnd$u002Eorange$u002Eindata$lzy1"}, {"name":"vnd$u002Eosa$u002Enetdeploy$lzy1"}, {"name":"vnd$u002Eosgeo$u002Emapguide$u002Epackage$lzy1"}, {"name":"vnd$u002Eosgi$u002Ebundle$lzy1"}, {"name":"vnd$u002Eosgi$u002Edp$lzy1"}, {"name":"vnd$u002Eosgi$u002Esubsystem$lzy1"}, {"name":"vnd$u002Eotps$u002Ect$minuskip$plusxml$lzy1"}, {"name":"vnd$u002Eoxli$u002Ecountgraph$lzy1"}, {"name":"vnd$u002Epagerduty$plusjson$lzy1"}, {"name":"vnd$u002Epalm$lzy1"}, {"name":"vnd$u002Epanoply$lzy1"}, {"name":"vnd$u002Epaos$u002Exml$lzy1"}, {"name":"vnd$u002Epatentdive$lzy1"}, {"name":"vnd$u002Epatientecommsdoc$lzy1"}, {"name":"vnd$u002Epawaafile$lzy1"}, {"name":"vnd$u002Epcos$lzy1"}, {"name":"vnd$u002Epg$u002Eformat$lzy1"}, {"name":"vnd$u002Epg$u002Eosasli$lzy1"}, {"name":"vnd$u002Epiaccess$u002Eapplication$minuslicence$lzy1"}, {"name":"vnd$u002Epicsel$lzy1"}, {"name":"vnd$u002Epmi$u002Ewidget$lzy1"}, {"name":"vnd$u002Epoc$u002Egroup$minusadvertisement$plusxml$lzy1"}, {"name":"vnd$u002Epocketlearn$lzy1"}, {"name":"vnd$u002Epowerbuilder6$lzy1"}, {"name":"vnd$u002Epowerbuilder6$minuss$lzy1"}, {"name":"vnd$u002Epowerbuilder7$lzy1"}, {"name":"vnd$u002Epowerbuilder7$minuss$lzy1"}, {"name":"vnd$u002Epowerbuilder75$lzy1"}, {"name":"vnd$u002Epowerbuilder75$minuss$lzy1"}, {"name":"vnd$u002Epreminet$lzy1"}, {"name":"vnd$u002Epreviewsystems$u002Ebox$lzy1"}, {"name":"vnd$u002Eproteus$u002Emagazine$lzy1"}, {"name":"vnd$u002Epsfs$lzy1"}, {"name":"vnd$u002Epublishare$minusdelta$minustree$lzy1"}, {"name":"vnd$u002Epvi$u002Eptid1$lzy1"}, {"name":"vnd$u002Epwg$minusmultiplexed$lzy1"}, {"name":"vnd$u002Epwg$minusxhtml$minusprint$plusxml$lzy1"}, {"name":"vnd$u002Equalcomm$u002Ebrew$minusapp$minusres$lzy1"}, {"name":"vnd$u002Equarantainenet$lzy1"}, {"name":"vnd$u002Equark$u002Equarkxpress$lzy1"}, {"name":"vnd$u002Equobject$minusquoxdocument$lzy1"}, {"name":"vnd$u002Eradisys$u002Emoml$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusaudit$minusconf$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusaudit$minusconn$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusaudit$minusdialog$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusaudit$minusstream$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusaudit$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusconf$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$minusbase$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$minusfax$minusdetect$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$minusfax$minussendrecv$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$minusgroup$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$minusspeech$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$minustransform$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusdialog$plusxml$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$plusxml$lzy1"}, {"name":"vnd$u002Erainstor$u002Edata$lzy1"}, {"name":"vnd$u002Erapid$lzy1"}, {"name":"vnd$u002Erar$lzy1"}, {"name":"vnd$u002Erealvnc$u002Ebed$lzy1"}, {"name":"vnd$u002Erecordare$u002Emusicxml$lzy1"}, {"name":"vnd$u002Erecordare$u002Emusicxml$plusxml$lzy1"}, {"name":"vnd$u002Erenlearn$u002Erlprint$lzy1"}, {"name":"vnd$u002Eresilient$u002Elogic$lzy1"}, {"name":"vnd$u002Erestful$plusjson$lzy1"}, {"name":"vnd$u002Erig$u002Ecryptonote$lzy1"}, {"name":"vnd$u002Erim$u002Ecod$lzy1"}, {"name":"vnd$u002Ern$minusrealmedia$lzy1"}, {"name":"vnd$u002Ern$minusrealmedia$minusvbr$lzy1"}, {"name":"vnd$u002Eroute66$u002Elink66$plusxml$lzy1"}, {"name":"vnd$u002Ers$minus274x$lzy1"}, {"name":"vnd$u002Eruckus$u002Edownload$lzy1"}, {"name":"vnd$u002Es3sms$lzy1"}, {"name":"vnd$u002Esailingtracker$u002Etrack$lzy1"}, {"name":"vnd$u002Esar$lzy1"}, {"name":"vnd$u002Esbm$u002Ecid$lzy1"}, {"name":"vnd$u002Esbm$u002Emid2$lzy1"}, {"name":"vnd$u002Escribus$lzy1"}, {"name":"vnd$u002Esealed$u002E3df$lzy1"}, {"name":"vnd$u002Esealed$u002Ecsf$lzy1"}, {"name":"vnd$u002Esealed$u002Edoc$lzy1"}, {"name":"vnd$u002Esealed$u002Eeml$lzy1"}, {"name":"vnd$u002Esealed$u002Emht$lzy1"}, {"name":"vnd$u002Esealed$u002Enet$lzy1"}, {"name":"vnd$u002Esealed$u002Eppt$lzy1"}, {"name":"vnd$u002Esealed$u002Etiff$lzy1"}, {"name":"vnd$u002Esealed$u002Exls$lzy1"}, {"name":"vnd$u002Esealedmedia$u002Esoftseal$u002Ehtml$lzy1"}, {"name":"vnd$u002Esealedmedia$u002Esoftseal$u002Epdf$lzy1"}, {"name":"vnd$u002Eseemail$lzy1"}, {"name":"vnd$u002Eseis$plusjson$lzy1"}, {"name":"vnd$u002Esema$lzy1"}, {"name":"vnd$u002Esemd$lzy1"}, {"name":"vnd$u002Esemf$lzy1"}, {"name":"vnd$u002Eshade$minussave$minusfile$lzy1"}, {"name":"vnd$u002Eshana$u002Einformed$u002Eformdata$lzy1"}, {"name":"vnd$u002Eshana$u002Einformed$u002Eformtemplate$lzy1"}, {"name":"vnd$u002Eshana$u002Einformed$u002Einterchange$lzy1"}, {"name":"vnd$u002Eshana$u002Einformed$u002Epackage$lzy1"}, {"name":"vnd$u002Eshootproof$plusjson$lzy1"}, {"name":"vnd$u002Eshopkick$plusjson$lzy1"}, {"name":"vnd$u002Eshp$lzy1"}, {"name":"vnd$u002Eshx$lzy1"}, {"name":"vnd$u002Esigrok$u002Esession$lzy1"}, {"name":"vnd$u002Esimtech$minusmindmapper$lzy1"}, {"name":"vnd$u002Esiren$plusjson$lzy1"}, {"name":"vnd$u002Esmaf$lzy1"}, {"name":"vnd$u002Esmart$u002Enotebook$lzy1"}, {"name":"vnd$u002Esmart$u002Eteacher$lzy1"}, {"name":"vnd$u002Esnesdev$minuspage$minustable$lzy1"}, {"name":"vnd$u002Esoftware602$u002Efiller$u002Eform$minusxml$minuszip$lzy1"}, {"name":"vnd$u002Esoftware602$u002Efiller$u002Eform$plusxml$lzy1"}, {"name":"vnd$u002Esolent$u002Esdkm$plusxml$lzy1"}, {"name":"vnd$u002Espotfire$u002Edxp$lzy1"}, {"name":"vnd$u002Espotfire$u002Esfs$lzy1"}, {"name":"vnd$u002Esqlite3$lzy1"}, {"name":"vnd$u002Esss$minuscod$lzy1"}, {"name":"vnd$u002Esss$minusdtf$lzy1"}, {"name":"vnd$u002Esss$minusntf$lzy1"}, {"name":"vnd$u002Estardivision$u002Ecalc$lzy1"}, {"name":"vnd$u002Estardivision$u002Edraw$lzy1"}, {"name":"vnd$u002Estardivision$u002Eimpress$lzy1"}, {"name":"vnd$u002Estardivision$u002Emath$lzy1"}, {"name":"vnd$u002Estardivision$u002Ewriter$lzy1"}, {"name":"vnd$u002Estardivision$u002Ewriter$minusglobal$lzy1"}, {"name":"vnd$u002Estepmania$u002Epackage$lzy1"}, {"name":"vnd$u002Estepmania$u002Estepchart$lzy1"}, {"name":"vnd$u002Estreet$minusstream$lzy1"}, {"name":"vnd$u002Esun$u002Ewadl$plusxml$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Ecalc$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Ecalc$u002Etemplate$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Edraw$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Edraw$u002Etemplate$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Eimpress$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Eimpress$u002Etemplate$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Emath$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Ewriter$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Ewriter$u002Eglobal$lzy1"}, {"name":"vnd$u002Esun$u002Exml$u002Ewriter$u002Etemplate$lzy1"}, {"name":"vnd$u002Esus$minuscalendar$lzy1"}, {"name":"vnd$u002Esvd$lzy1"}, {"name":"vnd$u002Eswiftview$minusics$lzy1"}, {"name":"vnd$u002Esycle$plusxml$lzy1"}, {"name":"vnd$u002Esyft$plusjson$lzy1"}, {"name":"vnd$u002Esymbian$u002Einstall$lzy1"}, {"name":"vnd$u002Esyncml$plusxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Edm$pluswbxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Edm$plusxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Edm$u002Enotification$lzy1"}, {"name":"vnd$u002Esyncml$u002Edmddf$pluswbxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Edmddf$plusxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Edmtnds$pluswbxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Edmtnds$plusxml$lzy1"}, {"name":"vnd$u002Esyncml$u002Eds$u002Enotification$lzy1"}, {"name":"vnd$u002Etableschema$plusjson$lzy1"}, {"name":"vnd$u002Etao$u002Eintent$minusmodule$minusarchive$lzy1"}, {"name":"vnd$u002Etcpdump$u002Epcap$lzy1"}, {"name":"vnd$u002Ethink$minuscell$u002Eppttc$plusjson$lzy1"}, {"name":"vnd$u002Etmd$u002Emediaflex$u002Eapi$plusxml$lzy1"}, {"name":"vnd$u002Etml$lzy1"}, {"name":"vnd$u002Etmobile$minuslivetv$lzy1"}, {"name":"vnd$u002Etri$u002Eonesource$lzy1"}, {"name":"vnd$u002Etrid$u002Etpt$lzy1"}, {"name":"vnd$u002Etriscape$u002Emxs$lzy1"}, {"name":"vnd$u002Etrueapp$lzy1"}, {"name":"vnd$u002Etruedoc$lzy1"}, {"name":"vnd$u002Eubisoft$u002Ewebplayer$lzy1"}, {"name":"vnd$u002Eufdl$lzy1"}, {"name":"vnd$u002Euiq$u002Etheme$lzy1"}, {"name":"vnd$u002Eumajin$lzy1"}, {"name":"vnd$u002Eunity$lzy1"}, {"name":"vnd$u002Euoml$plusxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Ealert$lzy1"}, {"name":"vnd$u002Euplanet$u002Ealert$minuswbxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Ebearer$minuschoice$lzy1"}, {"name":"vnd$u002Euplanet$u002Ebearer$minuschoice$minuswbxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Ecacheop$lzy1"}, {"name":"vnd$u002Euplanet$u002Ecacheop$minuswbxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Echannel$lzy1"}, {"name":"vnd$u002Euplanet$u002Echannel$minuswbxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Elist$lzy1"}, {"name":"vnd$u002Euplanet$u002Elist$minuswbxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Elistcmd$lzy1"}, {"name":"vnd$u002Euplanet$u002Elistcmd$minuswbxml$lzy1"}, {"name":"vnd$u002Euplanet$u002Esignal$lzy1"}, {"name":"vnd$u002Euri$minusmap$lzy1"}, {"name":"vnd$u002Evalve$u002Esource$u002Ematerial$lzy1"}, {"name":"vnd$u002Evcx$lzy1"}, {"name":"vnd$u002Evd$minusstudy$lzy1"}, {"name":"vnd$u002Evectorworks$lzy1"}, {"name":"vnd$u002Evel$plusjson$lzy1"}, {"name":"vnd$u002Everimatrix$u002Evcas$lzy1"}, {"name":"vnd$u002Everitone$u002Eaion$plusjson$lzy1"}, {"name":"vnd$u002Everyant$u002Ethin$lzy1"}, {"name":"vnd$u002Eves$u002Eencrypted$lzy1"}, {"name":"vnd$u002Evidsoft$u002Evidconference$lzy1"}, {"name":"vnd$u002Evisio$lzy1"}, {"name":"vnd$u002Evisionary$lzy1"}, {"name":"vnd$u002Evividence$u002Escriptfile$lzy1"}, {"name":"vnd$u002Evsf$lzy1"}, {"name":"vnd$u002Ewap$u002Esic$lzy1"}, {"name":"vnd$u002Ewap$u002Eslc$lzy1"}, {"name":"vnd$u002Ewap$u002Ewbxml$lzy1"}, {"name":"vnd$u002Ewap$u002Ewmlc$lzy1"}, {"name":"vnd$u002Ewap$u002Ewmlscriptc$lzy1"}, {"name":"vnd$u002Ewebturbo$lzy1"}, {"name":"vnd$u002Ewfa$u002Edpp$lzy1"}, {"name":"vnd$u002Ewfa$u002Ep2p$lzy1"}, {"name":"vnd$u002Ewfa$u002Ewsc$lzy1"}, {"name":"vnd$u002Ewindows$u002Edevicepairing$lzy1"}, {"name":"vnd$u002Ewmc$lzy1"}, {"name":"vnd$u002Ewmf$u002Ebootstrap$lzy1"}, {"name":"vnd$u002Ewolfram$u002Emathematica$lzy1"}, {"name":"vnd$u002Ewolfram$u002Emathematica$u002Epackage$lzy1"}, {"name":"vnd$u002Ewolfram$u002Eplayer$lzy1"}, {"name":"vnd$u002Ewordperfect$lzy1"}, {"name":"vnd$u002Ewqd$lzy1"}, {"name":"vnd$u002Ewrq$minushp3000$minuslabelled$lzy1"}, {"name":"vnd$u002Ewt$u002Estf$lzy1"}, {"name":"vnd$u002Ewv$u002Ecsp$pluswbxml$lzy1"}, {"name":"vnd$u002Ewv$u002Ecsp$plusxml$lzy1"}, {"name":"vnd$u002Ewv$u002Essp$plusxml$lzy1"}, {"name":"vnd$u002Exacml$plusjson$lzy1"}, {"name":"vnd$u002Exara$lzy1"}, {"name":"vnd$u002Exfdl$lzy1"}, {"name":"vnd$u002Exfdl$u002Ewebform$lzy1"}, {"name":"vnd$u002Exmi$plusxml$lzy1"}, {"name":"vnd$u002Exmpie$u002Ecpkg$lzy1"}, {"name":"vnd$u002Exmpie$u002Edpkg$lzy1"}, {"name":"vnd$u002Exmpie$u002Eplan$lzy1"}, {"name":"vnd$u002Exmpie$u002Eppkg$lzy1"}, {"name":"vnd$u002Exmpie$u002Exlim$lzy1"}, {"name":"vnd$u002Eyamaha$u002Ehv$minusdic$lzy1"}, {"name":"vnd$u002Eyamaha$u002Ehv$minusscript$lzy1"}, {"name":"vnd$u002Eyamaha$u002Ehv$minusvoice$lzy1"}, {"name":"vnd$u002Eyamaha$u002Eopenscoreformat$lzy1"}, {"name":"vnd$u002Eyamaha$u002Eopenscoreformat$u002Eosfpvg$plusxml$lzy1"}, {"name":"vnd$u002Eyamaha$u002Eremote$minussetup$lzy1"}, {"name":"vnd$u002Eyamaha$u002Esmaf$minusaudio$lzy1"}, {"name":"vnd$u002Eyamaha$u002Esmaf$minusphrase$lzy1"}, {"name":"vnd$u002Eyamaha$u002Ethrough$minusngn$lzy1"}, {"name":"vnd$u002Eyamaha$u002Etunnel$minusudpencap$lzy1"}, {"name":"vnd$u002Eyaoweme$lzy1"}, {"name":"vnd$u002Eyellowriver$minuscustom$minusmenu$lzy1"}, {"name":"vnd$u002Eyoutube$u002Eyt$lzy1"}, {"name":"vnd$u002Ezul$lzy1"}, {"name":"vnd$u002Ezzazz$u002Edeck$plusxml$lzy1"}, {"name":"voicexml$plusxml$lzy1"}, {"name":"voucher$minuscms$plusjson$lzy1"}, {"name":"vq$minusrtcpxr$lzy1"}, {"name":"wasm$lzy1"}, {"name":"watcherinfo$plusxml$lzy1"}, {"name":"webpush$minusoptions$plusjson$lzy1"}, {"name":"whoispp$minusquery$lzy1"}, {"name":"whoispp$minusresponse$lzy1"}, {"name":"widget$lzy1"}, {"name":"winhlp$lzy1"}, {"name":"wita$lzy1"}, {"name":"wordperfect5$u002E1$lzy1"}, {"name":"wsdl$plusxml$lzy1"}, {"name":"wspolicy$plusxml$lzy1"}, {"name":"x$minus7z$minuscompressed$lzy1"}, {"name":"x$minusabiword$lzy1"}, {"name":"x$minusace$minuscompressed$lzy1"}, {"name":"x$minusamf$lzy1"}, {"name":"x$minusapple$minusdiskimage$lzy1"}, {"name":"x$minusarj$lzy1"}, {"name":"x$minusauthorware$minusbin$lzy1"}, {"name":"x$minusauthorware$minusmap$lzy1"}, {"name":"x$minusauthorware$minusseg$lzy1"}, {"name":"x$minusbcpio$lzy1"}, {"name":"x$minusbdoc$lzy1"}, {"name":"x$minusbittorrent$lzy1"}, {"name":"x$minusblorb$lzy1"}, {"name":"x$minusbzip$lzy1"}, {"name":"x$minusbzip2$lzy1"}, {"name":"x$minuscbr$lzy1"}, {"name":"x$minuscdlink$lzy1"}, {"name":"x$minuscfs$minuscompressed$lzy1"}, {"name":"x$minuschat$lzy1"}, {"name":"x$minuschess$minuspgn$lzy1"}, {"name":"x$minuschrome$minusextension$lzy1"}, {"name":"x$minuscocoa$lzy1"}, {"name":"x$minuscompress$lzy1"}, {"name":"x$minusconference$lzy1"}, {"name":"x$minuscpio$lzy1"}, {"name":"x$minuscsh$lzy1"}, {"name":"x$minusdeb$lzy1"}, {"name":"x$minusdebian$minuspackage$lzy1"}, {"name":"x$minusdgc$minuscompressed$lzy1"}, {"name":"x$minusdirector$lzy1"}, {"name":"x$minusdoom$lzy1"}, {"name":"x$minusdtbncx$plusxml$lzy1"}, {"name":"x$minusdtbook$plusxml$lzy1"}, {"name":"x$minusdtbresource$plusxml$lzy1"}, {"name":"x$minusdvi$lzy1"}, {"name":"x$minusenvoy$lzy1"}, {"name":"x$minuseva$lzy1"}, {"name":"x$minusfont$minusbdf$lzy1"}, {"name":"x$minusfont$minusdos$lzy1"}, {"name":"x$minusfont$minusframemaker$lzy1"}, {"name":"x$minusfont$minusghostscript$lzy1"}, {"name":"x$minusfont$minuslibgrx$lzy1"}, {"name":"x$minusfont$minuslinux$minuspsf$lzy1"}, {"name":"x$minusfont$minuspcf$lzy1"}, {"name":"x$minusfont$minussnf$lzy1"}, {"name":"x$minusfont$minusspeedo$lzy1"}, {"name":"x$minusfont$minussunos$minusnews$lzy1"}, {"name":"x$minusfont$minustype1$lzy1"}, {"name":"x$minusfont$minusvfont$lzy1"}, {"name":"x$minusfreearc$lzy1"}, {"name":"x$minusfuturesplash$lzy1"}, {"name":"x$minusgca$minuscompressed$lzy1"}, {"name":"x$minusglulx$lzy1"}, {"name":"x$minusgnumeric$lzy1"}, {"name":"x$minusgramps$minusxml$lzy1"}, {"name":"x$minusgtar$lzy1"}, {"name":"x$minusgzip$lzy1"}, {"name":"x$minushdf$lzy1"}, {"name":"x$minushttpd$minusphp$lzy1"}, {"name":"x$minusinstall$minusinstructions$lzy1"}, {"name":"x$minusiso9660$minusimage$lzy1"}, {"name":"x$minusiwork$minuskeynote$minussffkey$lzy1"}, {"name":"x$minusiwork$minusnumbers$minussffnumbers$lzy1"}, {"name":"x$minusiwork$minuspages$minussffpages$lzy1"}, {"name":"x$minusjava$minusarchive$minusdiff$lzy1"}, {"name":"x$minusjava$minusjnlp$minusfile$lzy1"}, {"name":"x$minusjavascript$lzy1"}, {"name":"x$minuskeepass2$lzy1"}, {"name":"x$minuslatex$lzy1"}, {"name":"x$minuslua$minusbytecode$lzy1"}, {"name":"x$minuslzh$minuscompressed$lzy1"}, {"name":"x$minusmakeself$lzy1"}, {"name":"x$minusmie$lzy1"}, {"name":"x$minusmobipocket$minusebook$lzy1"}, {"name":"x$minusmpegurl$lzy1"}, {"name":"x$minusms$minusapplication$lzy1"}, {"name":"x$minusms$minusshortcut$lzy1"}, {"name":"x$minusms$minuswmd$lzy1"}, {"name":"x$minusms$minuswmz$lzy1"}, {"name":"x$minusms$minusxbap$lzy1"}, {"name":"x$minusmsaccess$lzy1"}, {"name":"x$minusmsbinder$lzy1"}, {"name":"x$minusmscardfile$lzy1"}, {"name":"x$minusmsclip$lzy1"}, {"name":"x$minusmsdos$minusprogram$lzy1"}, {"name":"x$minusmsdownload$lzy1"}, {"name":"x$minusmsmediaview$lzy1"}, {"name":"x$minusmsmetafile$lzy1"}, {"name":"x$minusmsmoney$lzy1"}, {"name":"x$minusmspublisher$lzy1"}, {"name":"x$minusmsschedule$lzy1"}, {"name":"x$minusmsterminal$lzy1"}, {"name":"x$minusmswrite$lzy1"}, {"name":"x$minusnetcdf$lzy1"}, {"name":"x$minusns$minusproxy$minusautoconfig$lzy1"}, {"name":"x$minusnzb$lzy1"}, {"name":"x$minusperl$lzy1"}, {"name":"x$minuspilot$lzy1"}, {"name":"x$minuspkcs12$lzy1"}, {"name":"x$minuspkcs7$minuscertificates$lzy1"}, {"name":"x$minuspkcs7$minuscertreqresp$lzy1"}, {"name":"x$minuspki$minusmessage$lzy1"}, {"name":"x$minusrar$minuscompressed$lzy1"}, {"name":"x$minusredhat$minuspackage$minusmanager$lzy1"}, {"name":"x$minusresearch$minusinfo$minussystems$lzy1"}, {"name":"x$minussea$lzy1"}, {"name":"x$minussh$lzy1"}, {"name":"x$minusshar$lzy1"}, {"name":"x$minusshockwave$minusflash$lzy1"}, {"name":"x$minussilverlight$minusapp$lzy1"}, {"name":"x$minussql$lzy1"}, {"name":"x$minusstuffit$lzy1"}, {"name":"x$minusstuffitx$lzy1"}, {"name":"x$minussubrip$lzy1"}, {"name":"x$minussv4cpio$lzy1"}, {"name":"x$minussv4crc$lzy1"}, {"name":"x$minust3vm$minusimage$lzy1"}, {"name":"x$minustads$lzy1"}, {"name":"x$minustar$lzy1"}, {"name":"x$minustcl$lzy1"}, {"name":"x$minustex$lzy1"}, {"name":"x$minustex$minustfm$lzy1"}, {"name":"x$minustexinfo$lzy1"}, {"name":"x$minustgif$lzy1"}, {"name":"x$minusustar$lzy1"}, {"name":"x$minusvirtualbox$minushdd$lzy1"}, {"name":"x$minusvirtualbox$minusova$lzy1"}, {"name":"x$minusvirtualbox$minusovf$lzy1"}, {"name":"x$minusvirtualbox$minusvbox$lzy1"}, {"name":"x$minusvirtualbox$minusvbox$minusextpack$lzy1"}, {"name":"x$minusvirtualbox$minusvdi$lzy1"}, {"name":"x$minusvirtualbox$minusvhd$lzy1"}, {"name":"x$minusvirtualbox$minusvmdk$lzy1"}, {"name":"x$minuswais$minussource$lzy1"}, {"name":"x$minusweb$minusapp$minusmanifest$plusjson$lzy1"}, {"name":"x$minuswww$minusform$minusurlencoded$lzy1"}, {"name":"x$minusx509$minusca$minuscert$lzy1"}, {"name":"x$minusx509$minusca$minusra$minuscert$lzy1"}, {"name":"x$minusx509$minusnext$minusca$minuscert$lzy1"}, {"name":"x$minusxfig$lzy1"}, {"name":"x$minusxliff$plusxml$lzy1"}, {"name":"x$minusxpinstall$lzy1"}, {"name":"x$minusxz$lzy1"}, {"name":"x$minuszmachine$lzy1"}, {"name":"x400$minusbp$lzy1"}, {"name":"xacml$plusxml$lzy1"}, {"name":"xaml$plusxml$lzy1"}, {"name":"xcap$minusatt$plusxml$lzy1"}, {"name":"xcap$minuscaps$plusxml$lzy1"}, {"name":"xcap$minusdiff$plusxml$lzy1"}, {"name":"xcap$minusel$plusxml$lzy1"}, {"name":"xcap$minuserror$plusxml$lzy1"}, {"name":"xcap$minusns$plusxml$lzy1"}, {"name":"xcon$minusconference$minusinfo$minusdiff$plusxml$lzy1"}, {"name":"xcon$minusconference$minusinfo$plusxml$lzy1"}, {"name":"xenc$plusxml$lzy1"}, {"name":"xhtml$minusvoice$plusxml$lzy1"}, {"name":"xhtml$plusxml$lzy1"}, {"name":"xliff$plusxml$lzy1"}, {"name":"xml$lzy1"}, {"name":"xml$minusdtd$lzy1"}, {"name":"xml$minusexternal$minusparsed$minusentity$lzy1"}, {"name":"xml$minuspatch$plusxml$lzy1"}, {"name":"xmpp$plusxml$lzy1"}, {"name":"xop$plusxml$lzy1"}, {"name":"xproc$plusxml$lzy1"}, {"name":"xslt$plusxml$lzy1"}, {"name":"xspf$plusxml$lzy1"}, {"name":"xv$plusxml$lzy1"}, {"name":"yang$lzy1"}, {"name":"yang$minusdata$plusjson$lzy1"}, {"name":"yang$minusdata$plusxml$lzy1"}, {"name":"yang$minuspatch$plusjson$lzy1"}, {"name":"yang$minuspatch$plusxml$lzy1"}, {"name":"yin$plusxml$lzy1"}, {"name":"zip$lzy1"}, {"name":"zlib$lzy1"}, {"name":"zstd$lzy1"}] +}, +{ + "name":"org.http4s.MimeDB$text$", + "fields":[{"name":"1d$minusinterleaved$minusparityfec$lzy3"}, {"name":"cache$minusmanifest$lzy1"}, {"name":"calendar$lzy1"}, {"name":"calender$lzy1"}, {"name":"cmd$lzy1"}, {"name":"coffeescript$lzy1"}, {"name":"cql$lzy1"}, {"name":"cql$minusexpression$lzy1"}, {"name":"cql$minusidentifier$lzy1"}, {"name":"css$lzy1"}, {"name":"csv$lzy1"}, {"name":"csv$minusschema$lzy1"}, {"name":"directory$lzy1"}, {"name":"dns$lzy2"}, {"name":"ecmascript$lzy2"}, {"name":"encaprtp$lzy3"}, {"name":"enriched$lzy1"}, {"name":"fhirpath$lzy1"}, {"name":"flexfec$lzy3"}, {"name":"fwdred$lzy2"}, {"name":"gff3$lzy1"}, {"name":"grammar$minusref$minuslist$lzy1"}, {"name":"html$lzy1"}, {"name":"jade$lzy1"}, {"name":"javascript$lzy2"}, {"name":"jcr$minuscnd$lzy1"}, {"name":"jsx$lzy1"}, {"name":"less$lzy1"}, {"name":"markdown$lzy1"}, {"name":"mathml$lzy1"}, {"name":"mdx$lzy1"}, {"name":"mizar$lzy1"}, {"name":"n3$lzy1"}, {"name":"parameters$lzy1"}, {"name":"parityfec$lzy3"}, {"name":"plain$lzy1"}, {"name":"provenance$minusnotation$lzy1"}, {"name":"prs$u002Efallenstein$u002Erst$lzy1"}, {"name":"prs$u002Elines$u002Etag$lzy1"}, {"name":"prs$u002Eprop$u002Elogic$lzy1"}, {"name":"raptorfec$lzy3"}, {"name":"red$lzy2"}, {"name":"rfc822$minusheaders$lzy1"}, {"name":"richtext$lzy1"}, {"name":"rtf$lzy2"}, {"name":"rtp$minusenc$minusaescm128$lzy2"}, {"name":"rtploopback$lzy3"}, {"name":"rtx$lzy3"}, {"name":"sgml$lzy2"}, {"name":"shaclc$lzy1"}, {"name":"shex$lzy1"}, {"name":"slim$lzy1"}, {"name":"spdx$lzy1"}, {"name":"strings$lzy1"}, {"name":"stylus$lzy1"}, {"name":"t140$lzy1"}, {"name":"tab$minusseparated$minusvalues$lzy1"}, {"name":"troff$lzy1"}, {"name":"turtle$lzy1"}, {"name":"ulpfec$lzy3"}, {"name":"uri$minuslist$lzy1"}, {"name":"vcard$lzy1"}, {"name":"vnd$u002Ea$lzy1"}, {"name":"vnd$u002Eabc$lzy1"}, {"name":"vnd$u002Eascii$minusart$lzy1"}, {"name":"vnd$u002Ecurl$lzy2"}, {"name":"vnd$u002Ecurl$u002Edcurl$lzy1"}, {"name":"vnd$u002Ecurl$u002Emcurl$lzy1"}, {"name":"vnd$u002Ecurl$u002Escurl$lzy1"}, {"name":"vnd$u002Edebian$u002Ecopyright$lzy1"}, {"name":"vnd$u002Edmclientscript$lzy1"}, {"name":"vnd$u002Edvb$u002Esubtitle$lzy2"}, {"name":"vnd$u002Eesmertec$u002Etheme$minusdescriptor$lzy1"}, {"name":"vnd$u002Efamilysearch$u002Egedcom$lzy1"}, {"name":"vnd$u002Eficlab$u002Eflt$lzy1"}, {"name":"vnd$u002Efly$lzy1"}, {"name":"vnd$u002Efmi$u002Eflexstor$lzy1"}, {"name":"vnd$u002Egml$lzy1"}, {"name":"vnd$u002Egraphviz$lzy1"}, {"name":"vnd$u002Ehans$lzy1"}, {"name":"vnd$u002Ehgl$lzy1"}, {"name":"vnd$u002Ein3d$u002E3dml$lzy1"}, {"name":"vnd$u002Ein3d$u002Espot$lzy1"}, {"name":"vnd$u002Eiptc$u002Enewsml$lzy1"}, {"name":"vnd$u002Eiptc$u002Enitf$lzy1"}, {"name":"vnd$u002Elatex$minusz$lzy1"}, {"name":"vnd$u002Emotorola$u002Ereflex$lzy1"}, {"name":"vnd$u002Ems$minusmediapackage$lzy1"}, {"name":"vnd$u002Enet2phone$u002Ecommcenter$u002Ecommand$lzy1"}, {"name":"vnd$u002Eradisys$u002Emsml$minusbasic$minuslayout$lzy1"}, {"name":"vnd$u002Esenx$u002Ewarpscript$lzy1"}, {"name":"vnd$u002Esi$u002Euricatalogue$lzy1"}, {"name":"vnd$u002Esosi$lzy1"}, {"name":"vnd$u002Esun$u002Ej2me$u002Eapp$minusdescriptor$lzy1"}, {"name":"vnd$u002Etrolltech$u002Elinguist$lzy1"}, {"name":"vnd$u002Ewap$u002Esi$lzy1"}, {"name":"vnd$u002Ewap$u002Esl$lzy1"}, {"name":"vnd$u002Ewap$u002Ewml$lzy1"}, {"name":"vnd$u002Ewap$u002Ewmlscript$lzy1"}, {"name":"vtt$lzy1"}, {"name":"x$minusasm$lzy1"}, {"name":"x$minusc$lzy1"}, {"name":"x$minuscomponent$lzy1"}, {"name":"x$minusfortran$lzy1"}, {"name":"x$minusgwt$minusrpc$lzy1"}, {"name":"x$minushandlebars$minustemplate$lzy1"}, {"name":"x$minusjava$minussource$lzy1"}, {"name":"x$minusjquery$minustmpl$lzy1"}, {"name":"x$minuslua$lzy1"}, {"name":"x$minusmarkdown$lzy1"}, {"name":"x$minusnfo$lzy1"}, {"name":"x$minusopml$lzy1"}, {"name":"x$minusorg$lzy1"}, {"name":"x$minuspascal$lzy1"}, {"name":"x$minusprocessing$lzy1"}, {"name":"x$minussass$lzy1"}, {"name":"x$minusscss$lzy1"}, {"name":"x$minussetext$lzy1"}, {"name":"x$minussfv$lzy1"}, {"name":"x$minussuse$minusymp$lzy1"}, {"name":"x$minusuuencode$lzy1"}, {"name":"x$minusvcalendar$lzy1"}, {"name":"x$minusvcard$lzy1"}, {"name":"xml$lzy2"}, {"name":"xml$minusexternal$minusparsed$minusentity$lzy2"}, {"name":"yaml$lzy1"}] +}, +{ + "name":"org.http4s.Query$", + "fields":[{"name":"parser$lzy1"}] +}, +{ + "name":"org.http4s.Query$Empty$", + "fields":[{"name":"multiParams$lzy1"}, {"name":"params$lzy1"}] +}, +{ + "name":"org.http4s.Query$Parsed", + "fields":[{"name":"multiParams$lzy3"}, {"name":"params$lzy3"}] +}, +{ + "name":"org.http4s.Uri", + "fields":[{"name":"renderString$lzy1"}] +}, +{ + "name":"org.http4s.Uri$", + "fields":[{"name":"SkipEncodeInPath$lzy1"}, {"name":"toSkip$lzy1"}] +}, +{ + "name":"org.http4s.Uri$Path$", + "fields":[{"name":"Asterisk$lzy1"}] +}, +{ + "name":"org.postgresql.Driver", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.postgresql.core.QueryExecutorCloseAction", + "fields":[{"name":"pgStream"}] +}, +{ + "name":"org.postgresql.jdbc.PgStatement", + "fields":[{"name":"cancelTimerTask"}, {"name":"isClosed"}, {"name":"statementState"}] +}, +{ + "name":"org.slf4j.Logger" +}, +{ + "name":"org.slf4j.spi.SLF4JServiceProvider" +}, +{ + "name":"org.typelevel.ci.package$CIStringSyntax", + "fields":[{"name":"ci$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.Server$", + "fields":[{"name":"cats$effect$IOApp$$queue$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.config.AppConfig$", + "fields":[{"name":"derived$ConfigReader$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.config.CorsConfig$", + "fields":[{"name":"derived$ConfigReader$lzy5"}] +}, +{ + "name":"ru.trett.rss.server.config.DbConfig$", + "fields":[{"name":"derived$ConfigReader$lzy3"}] +}, +{ + "name":"ru.trett.rss.server.config.GoogleConfig$", + "fields":[{"name":"derived$ConfigReader$lzy6"}] +}, +{ + "name":"ru.trett.rss.server.config.JobConfig$", + "fields":[{"name":"derived$ConfigReader$lzy7"}] +}, +{ + "name":"ru.trett.rss.server.config.JwtConfig$", + "fields":[{"name":"derived$ConfigReader$lzy8"}] +}, +{ + "name":"ru.trett.rss.server.config.OAuthConfig$", + "fields":[{"name":"derived$ConfigReader$lzy4"}] +}, +{ + "name":"ru.trett.rss.server.config.ServerConfig$", + "fields":[{"name":"derived$ConfigReader$lzy2"}] +}, +{ + "name":"ru.trett.rss.server.controllers.LoginController$", + "fields":[{"name":"oauthResponseEntityDecoder$lzy1"}, {"name":"userInfoResponseEntityDecoder$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.controllers.LogoutController", + "fields":[{"name":"as$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.controllers.UserController$", + "fields":[{"name":"given_Decoder_UserSettings$lzy1"}, {"name":"given_Encoder_UserSettings$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.repositories.ChannelRepository", + "fields":[{"name":"given_Read_Channel$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.repositories.FeedRepository", + "fields":[{"name":"given_Read_Feed$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.repositories.UserRepository", + "fields":[{"name":"given_Read_Option$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.services.ChannelService", + "fields":[{"name":"given_FeedParserRegistry_IO$lzy1"}] +}, +{ + "name":"ru.trett.rss.server.services.SummarizeService", + "fields":[{"name":"given_Decoder_GeminiResponse$lzy1"}] +}, +{ + "name":"scodec.bits.ByteVector", + "fields":[{"name":"hashCode$lzy1"}] +}, +{ + "name":"sun.management.ClassLoadingImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.CompilationImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.ManagementFactoryHelper$1", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.ManagementFactoryHelper$PlatformLoggingImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.MemoryImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.MemoryManagerImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.MemoryPoolImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.RuntimeImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.misc.Signal", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }, {"name":"handle","parameterTypes":["sun.misc.Signal","sun.misc.SignalHandler"] }] +}, +{ + "name":"sun.misc.SignalHandler" +}, +{ + "name":"sun.misc.Unsafe", + "fields":[{"name":"theUnsafe"}] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.JavaKeyStore$JKS", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA2$SHA224", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA2$SHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA5$SHA384", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA5$SHA512", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$DefaultSSLContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.IssuerAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] diff --git a/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/resource-config.json b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/resource-config.json new file mode 100644 index 0000000..2af4116 --- /dev/null +++ b/server/src/main/resources/META-INF/native-image/ru.trett.rss/server/resource-config.json @@ -0,0 +1,35 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.sql.Driver\\E" + }, { + "pattern":"\\QMETA-INF/services/org.flywaydb.core.extensibility.Plugin\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qapplication.conf\\E" + }, { + "pattern":"\\Qdb/migration/V1_0_0__Initial.sql\\E" + }, { + "pattern":"\\Qdb/migration/V1_0_1__Remove_retention_period_from_settings.sql\\E" + }, { + "pattern":"\\Qdb/migration/V1_0_2__Add_user_id_to_feeds.sql\\E" + }, { + "pattern":"\\Qdb/migration/V1_0_3__Add_highlighted_to_user_channels.sql\\E" + }, { + "pattern":"\\Qdb/migration/V1_0_4__Increase_varchar_limits.sql\\E" + }, { + "pattern":"\\Qdb/migration\\E" + }, { + "pattern":"\\Qlogback.xml\\E" + }, { + "pattern":"\\Qorg/flywaydb/core/internal/version.txt\\E" + }, { + "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfc.nrm\\E" + }, { + "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfkc.nrm\\E" + }, { + "pattern":"java.base:\\Qsun/net/www/content-types.properties\\E" + }]}, + "bundles":[] +} diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf index 9a52557..6d70518 100644 --- a/server/src/main/resources/application.conf +++ b/server/src/main/resources/application.conf @@ -40,6 +40,5 @@ jobs { } jwt { - secret = "change-me-in-production" - secret = ${?JWT_SECRET} + secret = ${JWT_SECRET} } diff --git a/server/src/main/scala/ru/trett/rss/server/Server.scala b/server/src/main/scala/ru/trett/rss/server/Server.scala index 7a5b89b..8f63f22 100644 --- a/server/src/main/scala/ru/trett/rss/server/Server.scala +++ b/server/src/main/scala/ru/trett/rss/server/Server.scala @@ -92,15 +92,9 @@ object Server extends IOApp: appConfig.google.apiKey ) channelService = ChannelService(channelRepository, client) - _ <- logger.info( - "Starting server on port: " + appConfig.server.port - ) + _ <- logger.info("Starting server on port: " + appConfig.server.port) authFilter <- AuthFilter[IO] - jobController = new JobController( - channelService, - userService, - appConfig.jobs - ) + jobController = new JobController(channelService, userService, appConfig.jobs) jarRoutes <- resourceServiceBuilder[IO]("/public").toRoutes appRoutes <- corsPolicy( @@ -147,7 +141,8 @@ object Server extends IOApp: hikariConfig.setJdbcUrl(config.url) hikariConfig.setUsername(config.user) hikariConfig.setPassword(config.password) - hikariConfig.setMaximumPoolSize(32) + hikariConfig.setMaximumPoolSize(10) + hikariConfig.setMinimumIdle(0) hikariConfig } xa <- HikariTransactor diff --git a/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala b/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala index 223f054..2959d15 100644 --- a/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala +++ b/server/src/main/scala/ru/trett/rss/server/authorization/AuthFilter.scala @@ -2,21 +2,24 @@ package ru.trett.rss.server.authorization import cats.data.* import cats.effect.* +import cats.effect.std.MapRef import cats.syntax.all.* -import com.github.blemale.scaffeine.{Cache, Scaffeine} import org.http4s.* import org.http4s.server.* import ru.trett.rss.server.models.User import ru.trett.rss.server.services.UserService -import scala.concurrent.duration.* +import java.util.concurrent.ConcurrentHashMap -class AuthFilter[F[_]: Sync: LiftIO] private (cache: Cache[String, User]): +class AuthFilter[F[_]: Sync: LiftIO]: + + private val cache: MapRef[F, String, Option[User]] = + MapRef.fromConcurrentHashMap(new ConcurrentHashMap[String, User]) def middleware(jwtManager: JwtManager, userService: UserService): AuthMiddleware[F, User] = AuthMiddleware(authUser(jwtManager, userService)) - def updateCache(user: User): F[Unit] = Sync[F].delay(cache.put(user.email, user)) + def updateCache(user: User): F[Unit] = cache.updateKeyValueIfSet(user.email, _ => user) private def authUser( jwtManager: JwtManager, @@ -35,22 +38,17 @@ class AuthFilter[F[_]: Sync: LiftIO] private (cache: Cache[String, User]): } private def getUser(email: String, userService: UserService): F[Option[User]] = - Sync[F].delay(cache.getIfPresent(email)).flatMap { + cache(email).get.flatMap { case Some(user) => user.some.pure[F] case None => LiftIO[F] .liftIO(userService.getUserByEmail(email)) .flatMap { case Some(user) => - Sync[F].delay(cache.put(email, user)).as(user.some) + cache(email).updateAndGet(_ => Some(user)) case None => none[User].pure[F] } } object AuthFilter: - def apply[F[_]: Sync: LiftIO]: F[AuthFilter[F]] = - val cache: Cache[String, User] = Scaffeine() - .maximumSize(100) - .expireAfterWrite(1.hour) - .build[String, User]() - new AuthFilter(cache).pure[F] + def apply[F[_]: Sync: LiftIO]: F[AuthFilter[F]] = new AuthFilter().pure[F] diff --git a/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala b/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala index 8b4b682..8f9c19d 100644 --- a/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala +++ b/server/src/main/scala/ru/trett/rss/server/controllers/JobController.scala @@ -19,7 +19,7 @@ class JobController(channelService: ChannelService, userService: UserService, co def routes: HttpRoutes[IO] = HttpRoutes.of[IO] { case req @ POST -> Root / "api" / "jobs" / "update" => val token = req.headers.get(ci"Authorization").map(_.head.value) - if (config.token.nonEmpty && !token.contains(s"Bearer ${config.token}")) { + if (config.token.isEmpty || !token.contains(s"Bearer ${config.token}")) { logger.warn("Unauthorized job update attempt") *> Forbidden("Invalid token") } else { for { diff --git a/server/src/main/scala/ru/trett/rss/server/db/FlywayMigration.scala b/server/src/main/scala/ru/trett/rss/server/db/FlywayMigration.scala index 3789738..c9324af 100644 --- a/server/src/main/scala/ru/trett/rss/server/db/FlywayMigration.scala +++ b/server/src/main/scala/ru/trett/rss/server/db/FlywayMigration.scala @@ -1,17 +1,57 @@ package ru.trett.rss.server.db -import cats.effect.IO +import cats.effect.* +import cats.implicits.* import org.flywaydb.core.Flyway import ru.trett.rss.server.config.DbConfig +import java.nio.file.{Files, Path} +import java.util.Comparator object FlywayMigration: def migrate(config: DbConfig): IO[Unit] = { - IO { - Flyway - .configure() - .dataSource(config.url, config.user, config.password) - .locations("db/migration") - .load() - .migrate() - }.void + Resource + .make(createTempDirWithMigrations())(deleteTempDir) + .use { tempDir => + IO { + Flyway + .configure() + .dataSource(config.url, config.user, config.password) + .locations(s"filesystem:${tempDir.toAbsolutePath}") + .connectRetries(3) + .load() + .migrate() + } + } + .void + } + + private def createTempDirWithMigrations(): IO[Path] = + for { + tempDir <- IO(Files.createTempDirectory("migrations")) + _ <- MigrationFiles.list.traverse { name => + val resourcePath = s"/db/migration/$name" + IO(Option(getClass.getResourceAsStream(resourcePath))).flatMap { + case None => + IO.raiseError( + new RuntimeException(s"Migration resource not found: $resourcePath") + ) + case Some(is) => + Resource.fromAutoCloseable(IO.pure(is)).use { stream => + IO(Files.copy(stream, tempDir.resolve(name))) + } + } + } + } yield tempDir + + private def deleteTempDir(path: Path): IO[Unit] = IO { + if (Files.exists(path)) { + val stream = Files.walk(path) + try { + stream + .sorted(Comparator.reverseOrder()) + .forEach(Files.delete) + } finally { + stream.close() + } + } } diff --git a/server/src/test/scala/ru/trett/rss/server/controllers/JobControllerSpec.scala b/server/src/test/scala/ru/trett/rss/server/controllers/JobControllerSpec.scala new file mode 100644 index 0000000..b87e7bb --- /dev/null +++ b/server/src/test/scala/ru/trett/rss/server/controllers/JobControllerSpec.scala @@ -0,0 +1,77 @@ +package ru.trett.rss.server.controllers + +import cats.effect.* +import cats.effect.unsafe.implicits.global +import org.http4s.* +import org.http4s.implicits.* +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.typelevel.log4cats.LoggerFactory +import org.typelevel.log4cats.slf4j.Slf4jFactory +import ru.trett.rss.server.config.JobConfig +import ru.trett.rss.server.services.ChannelService +import ru.trett.rss.server.services.UserService +import org.scalamock.scalatest.MockFactory +import org.http4s.headers.Authorization + +class JobControllerSpec extends AnyFunSuite with Matchers with MockFactory { + + implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO] + + private def createMocks() = { + val userService = new UserService(mock[ru.trett.rss.server.repositories.UserRepository]) { + override def getUsers: IO[List[ru.trett.rss.server.models.User]] = IO.pure(Nil) + } + val channelService = new ChannelService( + mock[ru.trett.rss.server.repositories.ChannelRepository], + mock[org.http4s.client.Client[IO]] + ) {} + (userService, channelService) + } + + test("POST /api/jobs/update returns Forbidden if token is empty and no header provided") { + val (mockUserService, mockChannelService) = createMocks() + val config = JobConfig("") + val controller = new JobController(mockChannelService, mockUserService, config) + + val request = Request[IO](Method.POST, uri"/api/jobs/update") + val response = controller.routes.orNotFound.run(request).unsafeRunSync() + + response.status shouldBe Status.Forbidden + } + + test("POST /api/jobs/update returns Forbidden if token is configured and header is missing") { + val (mockUserService, mockChannelService) = createMocks() + val config = JobConfig("secret") + val controller = new JobController(mockChannelService, mockUserService, config) + + val request = Request[IO](Method.POST, uri"/api/jobs/update") + val response = controller.routes.orNotFound.run(request).unsafeRunSync() + + response.status shouldBe Status.Forbidden + } + + test("POST /api/jobs/update returns Forbidden if token is configured and header is wrong") { + val (mockUserService, mockChannelService) = createMocks() + val config = JobConfig("secret") + val controller = new JobController(mockChannelService, mockUserService, config) + + val request = Request[IO](Method.POST, uri"/api/jobs/update") + .withHeaders(Authorization(Credentials.Token(AuthScheme.Bearer, "wrong"))) + val response = controller.routes.orNotFound.run(request).unsafeRunSync() + + response.status shouldBe Status.Forbidden + } + + test("POST /api/jobs/update returns Ok if token is configured and header is correct") { + val (mockUserService, mockChannelService) = createMocks() + val config = JobConfig("secret") + val controller = new JobController(mockChannelService, mockUserService, config) + + val request = Request[IO](Method.POST, uri"/api/jobs/update") + .withHeaders(Authorization(Credentials.Token(AuthScheme.Bearer, "secret"))) + val response = controller.routes.orNotFound.run(request).unsafeRunSync() + + response.status shouldBe Status.Ok + } +} diff --git a/server/src/test/scala/ru/trett/rss/server/utils/TestDatabase.scala b/server/src/test/scala/ru/trett/rss/server/utils/TestDatabase.scala index 59a2d95..ddedc7c 100644 --- a/server/src/test/scala/ru/trett/rss/server/utils/TestDatabase.scala +++ b/server/src/test/scala/ru/trett/rss/server/utils/TestDatabase.scala @@ -3,8 +3,8 @@ package ru.trett.rss.server.utils import cats.effect.{IO, Resource} import com.zaxxer.hikari.HikariConfig import doobie.hikari.HikariTransactor -import org.flywaydb.core.Flyway import org.testcontainers.containers.PostgreSQLContainer +import ru.trett.rss.server.db.FlywayMigration /** Utility for creating test databases with Testcontainers PostgreSQL. */ @@ -40,11 +40,7 @@ object TestDatabase: } yield xa private def runMigrations(jdbcUrl: String, username: String, password: String): IO[Unit] = - IO.blocking { - val flyway = Flyway - .configure() - .dataSource(jdbcUrl, username, password) - .locations("classpath:db/migration") - .load() - flyway.migrate() - }.void + FlywayMigration.migrate( + ru.trett.rss.server.config + .DbConfig("org.postgresql.Driver", jdbcUrl, username, password) + ) From 40161300a77d1a0c769c372c857f02910f4c6063 Mon Sep 17 00:00:00 2001 From: trett Date: Wed, 18 Feb 2026 22:33:10 +0100 Subject: [PATCH 4/6] refactor build file --- build.sbt | 174 +++++++++++++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 81 deletions(-) diff --git a/build.sbt b/build.sbt index d457069..5e8bd66 100644 --- a/build.sbt +++ b/build.sbt @@ -5,6 +5,8 @@ import com.typesafe.sbt.packager.docker.DockerApiVersion import scala.sys.process.* +// --- Variables --- + lazy val projectVersion = "2.4.4-gcr" lazy val organizationName = "ru.trett" lazy val scala3Version = "3.7.4" @@ -14,21 +16,39 @@ lazy val logs4catVersion = "2.7.1" lazy val doobieVersion = "1.0.0-RC11" lazy val customScalaOptions = Seq("-Wunused:imports", "-rewrite", "-source:3.4-migration") +// --- Task Keys --- + lazy val buildClientDist = taskKey[File]("Build client optimized package") lazy val buildImage = taskKey[Unit]("Build docker image") lazy val pushImage = taskKey[Unit]("Push docker image to remote repository") lazy val generateMigrationFiles = taskKey[Seq[File]]("Generate MigrationFiles.scala with list of SQL migrations") +lazy val generateFrontendAssets = taskKey[Seq[File]]("Build frontend and copy assets to managed resources") + +// --- Common Settings --- + +lazy val commonSettings = Seq( + version := projectVersion, + organization := organizationName, + scalaVersion := scala3Version, + scalacOptions ++= customScalaOptions, + Compile / packageDoc / mappings := Seq() +) + +inThisBuild( + List( + scalaVersion := scala3Version, + semanticdbEnabled := true, + semanticdbVersion := scalafixSemanticdb.revision + ) +) + +// --- Projects --- lazy val shared = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("shared")) - .settings( - name := "shared", - version := projectVersion, - organization := organizationName, - scalaVersion := scala3Version, - scalacOptions ++= customScalaOptions - ) + .settings(commonSettings) + .settings(name := "shared") .jsSettings() .jvmSettings() @@ -36,48 +56,62 @@ lazy val client = project .in(file("client")) .dependsOn(shared.js) .enablePlugins(ScalaJSPlugin, UniversalPlugin) + .settings(commonSettings) .settings( - version := projectVersion, - organization := organizationName, - scalaVersion := scala3Version, scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) .withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("client"))) }, Universal / mappings ++= directory(buildClientDist.value), - libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.1", - libraryDependencies += "com.raquo" %%% "laminar" % "17.2.1", - libraryDependencies += "be.doeraene" %%% "web-components-ui5" % "2.12.1", - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.6.0", + libraryDependencies ++= Seq( + "org.scala-js" %%% "scalajs-dom" % "2.8.1", + "com.raquo" %%% "laminar" % "17.2.1", + "be.doeraene" %%% "web-components-ui5" % "2.12.1", + "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.6.0" + ), libraryDependencies ++= Seq( "io.circe" %%% "circe-core", "io.circe" %%% "circe-generic", "io.circe" %%% "circe-parser" - ).map(_ % circeVersion), - scalacOptions ++= customScalaOptions, - Compile / packageDoc / mappings := Seq(), - inThisBuild( - List( - scalaVersion := scala3Version, - semanticdbEnabled := true, - semanticdbVersion := scalafixSemanticdb.revision - ) - ) + ).map(_ % circeVersion) ) lazy val server = project .in(file("server")) .dependsOn(shared.jvm) .enablePlugins(JavaAppPackaging, DockerPlugin, GraalVMNativeImagePlugin) + .settings(commonSettings) .settings( - version := projectVersion, - organization := organizationName, - scalaVersion := scala3Version, name := "server", + Compile / run / fork := true, + watchSources ++= (client / Compile / watchSources).value, + Compile / sourceGenerators += generateMigrationFiles.taskValue, + Compile / resourceGenerators += generateFrontendAssets.taskValue, + // GraalVM Native Image Settings + graalVMNativeImageOptions ++= Seq( + "--no-fallback", + "-H:+ReportExceptionStackTraces", + "--verbose", + "--enable-https", + "--enable-http", + "-H:IncludeResources=application\\.conf", + "-H:IncludeResources=logback\\.xml", + "-H:IncludeResources=public/.*", + "-H:IncludeResources=db/migration/.*", + "-H:DeadlockWatchdogInterval=900", + "-Ob", + "-J-Xmx24G", + "-R:MaxHeapSize=512m", + "--initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback,com.fasterxml.jackson", + "--initialize-at-run-time=io.netty.channel.epoll.Epoll,io.netty.channel.epoll.Native,io.netty.channel.epoll.EpollEventLoop,io.netty.channel.epoll.EpollEventLoopGroup,io.netty.channel.kqueue.KQueue,io.netty.channel.kqueue.Native,io.netty.channel.kqueue.KQueueEventLoopGroup,org.http4s.MimeDB" + ), + // Docker Settings dockerPermissionStrategy := DockerPermissionStrategy.None, dockerBaseImage := "debian:12-slim", dockerApiVersion := Some(DockerApiVersion(1, 40)), + dockerRepository := sys.env.get("REGISTRY"), + dockerExposedPorts := Seq(8080), dockerCommands := { val commands = dockerCommands.value val filteredCommands = commands.filter { @@ -93,46 +127,28 @@ lazy val server = project filteredCommands ++ Seq( Cmd("RUN", "apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*"), Cmd("WORKDIR", "/opt/docker"), - ExecCmd( - "ENTRYPOINT", - "/opt/docker/bin/server" - ) + ExecCmd("ENTRYPOINT", "/opt/docker/bin/server") ) }, - dockerRepository := sys.env.get("REGISTRY"), - dockerExposedPorts := Seq(8080), Docker / mappings := { val nativeImage = (GraalVMNativeImage / packageBin).value val standardMappings = (Docker / mappings).value - standardMappings.filter { case (file, path) => + standardMappings.filter { case (_, path) => !path.contains("bin/server") && !path.contains("lib/") } :+ (nativeImage -> "/opt/docker/bin/server") }, - graalVMNativeImageOptions ++= Seq( - "--no-fallback", - "-H:+ReportExceptionStackTraces", - "--verbose", - "--enable-https", - "--enable-http", - "-H:IncludeResources=application\\.conf", - "-H:IncludeResources=logback\\.xml", - "-H:IncludeResources=public/.*", - "-H:IncludeResources=db/migration/.*", - "-H:DeadlockWatchdogInterval=900", - "-Ob", - "-J-Xmx24G", - "-R:MaxHeapSize=512m", - "--initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback,com.fasterxml.jackson", - "--initialize-at-run-time=io.netty.channel.epoll.Epoll,io.netty.channel.epoll.Native,io.netty.channel.epoll.EpollEventLoop,io.netty.channel.epoll.EpollEventLoopGroup,io.netty.channel.kqueue.KQueue,io.netty.channel.kqueue.Native,io.netty.channel.kqueue.KQueueEventLoopGroup,org.http4s.MimeDB" - ), - watchSources ++= (client / Compile / watchSources).value, - Compile / sourceGenerators += generateMigrationFiles.taskValue, + // Dependencies libraryDependencies ++= Seq( "org.typelevel" %% "cats-effect" % "3.6.3", "org.slf4j" % "slf4j-api" % "2.0.17", "ch.qos.logback" % "logback-classic" % "1.5.25", "org.flywaydb" % "flyway-core" % "11.17.2", - "com.github.pureconfig" %% "pureconfig-core" % "0.17.9" + "com.github.pureconfig" %% "pureconfig-core" % "0.17.9", + "org.jsoup" % "jsoup" % "1.21.2", + "io.circe" %% "circe-fs2" % "0.14.1", + "com.github.jwt-scala" %% "jwt-circe" % "10.0.1", + "com.google.cloud.sql" % "postgres-socket-factory" % "1.15.1", + "org.flywaydb" % "flyway-database-postgresql" % "11.17.2" % "runtime" ), libraryDependencies ++= Seq( "org.http4s" %% "http4s-ember-server", @@ -155,42 +171,27 @@ lazy val server = project "org.tpolecat" %% "doobie-postgres", "org.tpolecat" %% "doobie-postgres-circe" ).map(_ % doobieVersion), - libraryDependencies += "org.jsoup" % "jsoup" % "1.21.2", - libraryDependencies += "io.circe" %% "circe-fs2" % "0.14.1", - libraryDependencies += "com.github.jwt-scala" %% "jwt-circe" % "10.0.1", - libraryDependencies += "com.google.cloud.sql" % "postgres-socket-factory" % "1.15.1", - libraryDependencies += "org.flywaydb" % "flyway-database-postgresql" % "11.17.2" % "runtime", - libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test, - libraryDependencies += "org.scalamock" %% "scalamock" % "7.5.2" % Test, - libraryDependencies += "org.testcontainers" % "testcontainers" % "2.0.2" % Test, - libraryDependencies += "org.testcontainers" % "postgresql" % "1.21.3" % Test, - libraryDependencies += "org.postgresql" % "postgresql" % "42.7.8" % Test, - scalacOptions ++= customScalaOptions, - Compile / run / fork := true, - Compile / packageDoc / mappings := Seq(), - Compile / resourceGenerators += Def.task { - val _ = (client / Compile / fullLinkJS).value - val distDir = buildClientDist.value - val targetDir = (Compile / resourceManaged).value / "public" - IO.copyDirectory(distDir, targetDir) - (targetDir ** "*").get - }.taskValue, - inThisBuild( - List( - scalaVersion := scala3Version, - semanticdbEnabled := true, - semanticdbVersion := scalafixSemanticdb.revision - ) + libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "3.2.19" % Test, + "org.scalamock" %% "scalamock" % "7.5.2" % Test, + "org.testcontainers" % "testcontainers" % "2.0.2" % Test, + "org.testcontainers" % "postgresql" % "1.21.3" % Test, + "org.postgresql" % "postgresql" % "42.7.8" % Test ) ) + +// --- Task Implementations --- + ThisBuild / buildClientDist := { Process("npm install", client.base).! Process("npm run build", client.base).! client.base / "dist" } + buildImage := { (server / Docker / publishLocal).value } + pushImage := { (server / Docker / publish).value } @@ -202,13 +203,13 @@ server / generateMigrationFiles := { val resourceDir = (server / Compile / resourceDirectory).value val migrationDir = resourceDir / "db" / "migration" - // List all .sql files in the migration directory val migrations = if (migrationDir.exists()) { migrationDir.listFiles().filter(_.getName.endsWith(".sql")).map(_.getName).toList.sorted } else Nil val file = (server / Compile / sourceManaged).value / "ru" / "trett" / "rss" / "server" / "db" / "MigrationFiles.scala" + val migrationsStr = migrations.map(m => "\"" + m + "\"").mkString(", ") val content = s"""package ru.trett.rss.server.db @@ -225,3 +226,14 @@ server / generateMigrationFiles := { IO.write(file, content) Seq(file) } + +// Task to build the Scala.js frontend and copy the resulting assets (JS, CSS, HTML, etc.) +// from the client project to the server's managed resources directory. +// This allows the server to serve the frontend as static content. +server / generateFrontendAssets := { + val _ = (client / Compile / fullLinkJS).value + val distDir = buildClientDist.value + val targetDir = (server / Compile / resourceManaged).value / "public" + IO.copyDirectory(distDir, targetDir) + (targetDir ** "*").get +} From 6321fd2c6b42993f1bac4c48eb70d4ef40d6b92b Mon Sep 17 00:00:00 2001 From: trett Date: Thu, 19 Feb 2026 17:03:08 +0100 Subject: [PATCH 5/6] fix --- cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 40e0583..5c6dcec 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -27,7 +27,7 @@ steps: - name: gcr.io/cloud-builders/docker args: - push - - $_AR_HOSTNAMEv/$_AR_PROJECT_ID/$_AR_REPOSITORY/rssreader:$SHORT_SHA + - $_AR_HOSTNAME/$_AR_PROJECT_ID/$_AR_REPOSITORY/rssreader:$SHORT_SHA timeout: 1800s options: machineType: E2_HIGHCPU_32 From 3205537e56a8adfd3937a09a42606bf8e7896872 Mon Sep 17 00:00:00 2001 From: trett Date: Thu, 19 Feb 2026 17:41:06 +0100 Subject: [PATCH 6/6] fix --- cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 5c6dcec..22978d4 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -28,7 +28,7 @@ steps: args: - push - $_AR_HOSTNAME/$_AR_PROJECT_ID/$_AR_REPOSITORY/rssreader:$SHORT_SHA -timeout: 1800s +timeout: 2400s options: machineType: E2_HIGHCPU_32 logging: CLOUD_LOGGING_ONLY