All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sttp.tapir.files.Resources.scala Maven / Gradle / Ivy

The newest version!
package sttp.tapir.files

import sttp.model.MediaType
import sttp.monad.MonadError
import sttp.monad.syntax._

import java.io.File
import sttp.tapir.InputStreamRange
import sttp.tapir.RangeValue
import java.net.URL

import sttp.tapir.files.StaticInput
import Files._

object Resources {

  def head[F[_]](
      classLoader: ClassLoader,
      resourcePrefix: String,
      options: FilesOptions[F] = FilesOptions.default[F]
  ): MonadError[F] => StaticInput => F[Either[StaticErrorOutput, StaticOutput[Unit]]] = { implicit monad => filesInput =>
    get(classLoader, resourcePrefix, options)(monad)(filesInput)
      .map(_.map(_.withoutBody))
  }

  def get[F[_]](
      classLoader: ClassLoader,
      resourcePrefix: String,
      options: FilesOptions[F] = FilesOptions.default[F]
  ): MonadError[F] => StaticInput => F[Either[StaticErrorOutput, StaticOutput[InputStreamRange]]] = { implicit monad => filesInput =>
    val resolveUrlFn: ResolveUrlFn = resolveResourceUrl(classLoader, resourcePrefix.split("/").toList, filesInput, options)
    files(filesInput, options, resolveUrlFn, resourceRangeFromUrl)
  }

  private def resourceRangeFromUrl(
      url: URL,
      range: Option[RangeValue]
  ): InputStreamRange = InputStreamRange(
    () => url.openStream(),
    range
  )

  /** Creates a function of type ResolveUrlFn, which is capable of taking a relative path as a list of string segments, and finding the
    * actual full Url of a file available as a resource under a classLoader, considering additional parameters. For example, with a root
    * resource prefix of /config/files/ it can create a function which takies List("dir1", "dir2", "file.txt") and tries to resolve
    * /config/files/dir1/dir2/file.txt into a resource Url. The final resolved file may also be resolved to a pre-gzipped sibling, an
    * index.html file, or a default file given as a fallback, all depending on additional parameters. See also Files.resolveSystemPathUrl
    * for an equivalent of this function but for files from the filesystem.
    *
    * @param classLoader
    *   the class loader that will be used to call .getResource
    * @param resourcePrefix
    *   root resource path prefix represented as string segments
    * @param input
    *   request input parameters like path and headers, used together with options to apply filtering and look for possible pre-gzipped
    *   files if they are accepted
    * @param options
    *   additional options of the endpoint, defining filtering rules and pre-gzipped file support
    * @return
    *   a function which can be used in general file resolution logic. This function takes path segments and an optional default fallback
    *   path segments and tries to resolve the file, then returns its full Url.
    */
  private def resolveResourceUrl[F[_]](
      classLoader: ClassLoader,
      resourcePrefix: List[String],
      input: StaticInput,
      options: FilesOptions[F]
  ): ResolveUrlFn = {

    val nameComponents = resourcePrefix ++ input.path

    def resolveRec(path: List[String], default: Option[List[String]]): Option[ResolvedUrl] = {
      val name = (resourcePrefix ++ path).mkString("/")
      val resultOpt = (if (useGzippedIfAvailable(input, options))
                         Option(classLoader.getResource(name + ".gz")).map(ResolvedUrl(_, contentTypeFromName(name), Some("gzip")))
                       else None)
        .orElse(Option(classLoader.getResource(name)).map(ResolvedUrl(_, contentTypeFromName(name), None)))
        .orElse(default match {
          case None => None
          case Some(defaultPath) =>
            resolveRec(path = defaultPath, default = None)
        })
        // making sure that the resulting path contains the original requested path
        .filter(_.url.toURI.toString.contains(resourcePrefix.mkString("/")))

      if (resultOpt.exists(r => isDirectory(classLoader, name, r.url)))
        resolveRec(path :+ "index.html", default)
      else resultOpt
    }

    if (!options.fileFilter(nameComponents))
      (_, _) => LeftUrlNotFound
    else
      Function.untupled((resolveRec _).tupled.andThen(_.map(Right(_)).getOrElse(LeftUrlNotFound)))
  }

  private def isDirectory(classLoader: ClassLoader, name: String, nameResource: URL): Boolean = {
    // https://stackoverflow.com/questions/20105554/is-there-a-way-to-tell-if-a-classpath-resource-is-a-file-or-a-directory
    if (nameResource.getProtocol == "file") new File(nameResource.getPath).isDirectory
    else classLoader.getResource(name + "/") != null
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy