R/server.R

Defines functions log_track_message generate_cli_message parse_range guess_type read_raw generate_body resolve_req_path serve_directory create_server serve_data

Documented in serve_data

#' Serve a local data directory for use with a browser
#'
#' This is a utility function that can be used to server
#' a local directory with data so that it can be used
#' in the genome browser.
#'
#' Note: This is intended for local development and use.
#' For a production deployment, refer to the vignette
#' on creating URLs for more robust options.
#'
#' @param path The path to the directory with data to serve
#' @param port The port to serve the directory on
#'
#' @return a list containing information about the newly
#'   created HTTP server including the host, port, interval,
#'   and URL. The list also contains the stop_server()
#'   function which can be used to stop the server
#'
#' @export
#'
#' @examples
#' \dontrun{
#' server <- serve_data("~/path/to/my-data")
#' # use server$stop_server() to stop
#' }
serve_data <- function(path, port = 5000) {
  path <- normalizePath(path, mustWork = TRUE)

  if (path != ".") {
    # clean up R directory after exit if not current directory
    original_dir <- setwd(path)
    on.exit(setwd(original_dir))
  }

  generate_cli_message(path, port)

  server <- create_server(path, port)
  app <- list(call = serve_directory(path))
  server$start_server(app)
  invisible(server)
}

create_server <- function(path, port) {
  host <- "127.0.0.1"
  server <- NULL
  list(
    host = host,
    port = as.integer(port),
    interval = 1,
    url = sprintf("http://%s:%d", "127.0.0.1", as.integer(port)),
    start_server = function(app) {
      server_id <- httpuv::startServer(host, port, app)
      server <<- server_id
      invisible(server_id)
    },
    stop_server = function() {
      if (is.null(server)) {
        stop("The server has not been started.")
      } else {
        httpuv::stopServer(server)
      }
    }
  )
}

serve_directory <- function(path) {
  function(req) {
    original_dir <- setwd(path)
    on.exit(setwd(original_dir))
    req_path <- resolve_req_path(req)

    # response content
    if (is.null(req$HTTP_RANGE)) {
      status <- 200L
    } else {
      status <- 206L
      range <- parse_range(req$HTTP_RANGE, req_path)
    }
    type <- guess_type(path)
    body <- generate_body(req, req_path)
    if (is.character(body) && length(body) > 1) {
      body <- stringr::str_c(body, collapse = "")
    }
    headers <- c(
      list(
        "Access-Control-Allow-Origin" = "*",
        "Access-Control-Allow-Headers" = "range, Origin, Content-Type",
        "Access-Control-Expose-Headers" = "Content-Length,Content-Range",
        "Content-Type" = type
      ), if (status == 206L) {
        list(
          "Content-Range" = paste0(
            "bytes ",
            range[2],
            "-",
            range[3],
            "/",
            file.info(req_path)[, "size"]
          )
        )
      },
      "Accept-Ranges" = "bytes"
    )

    list(
      status = status,
      body = body,
      headers = headers
    )
  }
}

resolve_req_path <- function(req) {
  req_path <- httpuv::decodeURIComponent(req$PATH_INFO)
  Encoding(req_path) <- "UTF-8"

  if (grepl("^/", req_path)) {
    stringr::str_c(".", req_path)
  } else if (req_path == "") {
    "."
  }
}

generate_body <- function(req, req_path) {
  range <- req$HTTP_RANGE

  if (is.null(range)) {
    read_raw(req_path)
  } else {
    range <- parse_range(range, req_path)
    byte2 <- as.numeric(range[2])
    byte3 <- as.numeric(range[3])

    if (length(range) < 3 || (range[1] != "bytes") || (byte2 >= byte3)) {
      return(
        list(
          status = 416L, headers = list("Content-Type" = "text/plain"),
          body = "Requested range not satisfiable\r\n"
        )
      )
    }

    connection <- file(req_path, open = "rb", raw = TRUE)
    on.exit(close(connection))
    seek(connection, where = byte2, origin = "start")
    readBin(connection, "raw", byte3 - byte2 + 1)
  }
}

read_raw <- function(req_path) {
  readBin(
    req_path,
    "raw",
    file.info(req_path)[, "size"]
  )
}

guess_type <- function(path) {
  mimetype <- function(...) {
    system2("mimetype", c("-b", shQuote(path)), ...)
  }
  if (Sys.which("mimetype") == "" || mimetype(stdout = NULL) != 0) {
    return(mime::guess_type(path))
  }
  mimetype(stdout = TRUE)
}

parse_range <- function(range, req_path) {
  range <- strsplit(range, split = "(=|-)")[[1]]
  if (length(range) == 2 && range[1] == "bytes") {
    # open-end range request
    range[3] <- file.info(req_path)[, "size"] - 1
  }
  range
}

generate_cli_message <- function(path, port) {
  data_files <- dir(path)

  cli::cli_par()
  cli::cli_text("Serving data from: {path}")
  cli::cli_end()

  cli::cli_par()
  cli::cli_text("Use the following URLs for your track data:")
  cli::cli_end()

  assembly <- c()
  alignments <- c()
  feature <- c()
  variant <- c()
  wiggle <- c()

  for (i in seq_along(data_files)) {
    stripped_gz <- strip_gz(data_files[i])

    if (stringr::str_ends(stripped_gz, ".fa") || stringr::str_ends(stripped_gz, ".fasta")) {
      assembly <- c(assembly, stringr::str_glue("http://127.0.0.1:{port}/{data_files[i]}"))
    } else if (stringr::str_ends(stripped_gz, ".bam") || stringr::str_ends(stripped_gz, ".cram")) {
      alignments <- c(alignments, stringr::str_glue("http://127.0.0.1:{port}/{data_files[i]}"))
    } else if (stringr::str_ends(stripped_gz, ".gff") || stringr::str_ends(stripped_gz, ".gff3")) {
      feature <- c(feature, stringr::str_glue("http://127.0.0.1:{port}/{data_files[i]}"))
    } else if (stringr::str_ends(stripped_gz, ".vcf")) {
      variant <- c(variant, stringr::str_glue("http://127.0.0.1:{port}/{data_files[i]}"))
    } else if (stringr::str_ends(stripped_gz, ".bw") || stringr::str_ends(stripped_gz, ".bigWig")) {
      wiggle <- c(wiggle, stringr::str_glue("http://127.0.0.1:{port}/{data_files[i]}"))
    }
  }

  log_track_message("Assembly", assembly)
  log_track_message("Alignments", alignments)
  log_track_message("Feature", feature)
  log_track_message("Variant", variant)
  log_track_message("Wiggle", wiggle)

  cli::cli_par()
  cli::cli_alert_info("Note: this server is intended for local development and use.")
  cli::cli_text(
    "For a production deployment, refer to the vignette about creating URLs for a discussion of better options."
  )
  cli::cli_end()

  cli::cli_text("To stop the server, use the $stop_server() function.")
}

log_track_message <- function(title, track_vector, port) {
  if (length(track_vector) > 0) {
    cli::cli_par()
    cli::cli_h2(title)
    cli::cli_ul(track_vector)
    cli::cli_end()
  }
}

# Notes: ------------------------------------------------------------------

# This is a small static HTTP server for local files

# It is heavily inspired by the static server in the {servr} package by
# Yihui Xie

# There are two crucial requirements for serving data to JBrowse 2:

# 1. CORS must be enabled. See headers for more info.
# 2. Must support range requests

Try the JBrowseR package in your browser

Any scripts or data that you put into this service are public.

JBrowseR documentation built on June 8, 2023, 6:41 a.m.