tds/src/main/java/thredds/servlet/ServletUtil.java
public static void returnFile(HttpServletRequest req, HttpServletResponse res, File file, String contentType) throws IOException {
    res.setContentType(contentType);
    res.addDateHeader("Last-Modified", file.lastModified());
    // res.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");

    // see if its a Range Request
    boolean isRangeRequest = false;
    long startPos = 0, endPos = Long.MAX_VALUE;
    String rangeRequest = req.getHeader("Range");
    if (rangeRequest != null) { // bytes=12-34 or bytes=12-
      int pos = rangeRequest.indexOf("=");
      if (pos > 0) {
        int pos2 = rangeRequest.indexOf("-");
        if (pos2 > 0) {
          String startString = rangeRequest.substring(pos + 1, pos2);
          String endString = rangeRequest.substring(pos2 + 1);
          startPos = Long.parseLong(startString);
          if (endString.length() > 0)
            endPos = Long.parseLong(endString) + 1;
          isRangeRequest = true;
        }
      }
    }

    // set content length
    long fileSize = file.length();
    long contentLength = fileSize;
    if (isRangeRequest) {
      endPos = Math.min(endPos, fileSize);
      contentLength = endPos - startPos;
    }

    // when compression is turned on, ContentLength has to be overridden
    // this is also true for HEAD, since this must be the same as GET without the body
    if (contentLength > Integer.MAX_VALUE)
      res.addHeader("Content-Length", Long.toString(contentLength));  // allow content length > MAX_INT
    else
      res.setContentLength((int) contentLength);

    String filename = file.getPath();
    // indicate we allow Range Requests
    res.addHeader("Accept-Ranges", "bytes");

    if (req.getMethod().equals("HEAD")) {
      return;
    }

    try {

      if (isRangeRequest) {
        // set before content is sent
        res.addHeader("Content-Range", "bytes " + startPos + "-" + (endPos - 1) + "/" + fileSize);
        res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

        try (RandomAccessFile craf = RandomAccessFile.acquire(filename)) {
          IO.copyRafB(craf, startPos, contentLength, res.getOutputStream(), new byte[60000]);
          return;
        }
      }

      // Return the file
      ServletOutputStream out = res.getOutputStream();
      IO.copyFileB(file, out, 60 * 1000);
      /* try (WritableByteChannel cOut = Channels.newChannel(out)) {
        IO.copyFileWithChannels(file, cOut);
        res.flushBuffer();
      } */
    }

    // @todo Split up this exception handling: those from file access vs those from dealing with response
    //       File access: catch and res.sendError()
    //       response: don't catch (let bubble up out of doGet() etc)
    catch (FileNotFoundException e) {
      log.error("returnFile(): FileNotFoundException= " + filename);
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_NOT_FOUND);
    } catch (java.net.SocketException e) {
      log.info("returnFile(): SocketException sending file: " + filename + " " + e.getMessage());
    } catch (IOException e) {
      String eName = e.getClass().getName(); // dont want compile time dependency on ClientAbortException
      if (eName.equals("org.apache.catalina.connector.ClientAbortException")) {
        log.debug("returnFile(): ClientAbortException while sending file: " + filename + " " + e.getMessage());
        return;
      }

      if (e.getMessage().startsWith("File transfer not complete")) { // coming from FileTransfer.transferTo()
        log.debug("returnFile() "+e.getMessage());
        return;
      }

      log.error("returnFile(): IOException (" + e.getClass().getName() + ") sending file ", e);
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_NOT_FOUND, "Problem sending file: " + e.getMessage());
    }
  }
public static void returnFile(HttpServletRequest req, HttpServletResponse res, File file, String contentType) throws IOException {
    res.setContentType(contentType);
    res.addDateHeader("Last-Modified", file.lastModified());
    // res.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");

    // see if its a Range Request
    boolean isRangeRequest = false;
    long startPos = 0, endPos = Long.MAX_VALUE;
    String rangeRequest = req.getHeader("Range");
    if (rangeRequest != null) { // bytes=12-34 or bytes=12-
      int pos = rangeRequest.indexOf("=");
      if (pos > 0) {
        int pos2 = rangeRequest.indexOf("-");
        if (pos2 > 0) {
          String startString = rangeRequest.substring(pos + 1, pos2);
          String endString = rangeRequest.substring(pos2 + 1);
          startPos = Long.parseLong(startString);
          if (endString.length() > 0)
            endPos = Long.parseLong(endString) + 1;
          isRangeRequest = true;
        }
      }
    }

    // set content length
    long fileSize = file.length();
    long contentLength = fileSize;
    if (isRangeRequest) {
      endPos = Math.min(endPos, fileSize);
      contentLength = endPos - startPos;
    }

    // when compression is turned on, ContentLength has to be overridden
    // this is also true for HEAD, since this must be the same as GET without the body
    if (contentLength > Integer.MAX_VALUE)
      res.addHeader("Content-Length", Long.toString(contentLength));  // allow content length > MAX_INT
    else
      res.setContentLength((int) contentLength);

    String filename = file.getPath();
    // indicate we allow Range Requests
    res.addHeader("Accept-Ranges", "bytes");

    if (req.getMethod().equals("HEAD")) {
      return;
    }

    try {

      if (isRangeRequest) {
        // set before content is sent
        res.addHeader("Content-Range", "bytes " + startPos + "-" + (endPos - 1) + "/" + fileSize);
        res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

        try (RandomAccessFile craf = RandomAccessFile.acquire(filename)) {
          IO.copyRafB(craf, startPos, contentLength, res.getOutputStream(), new byte[60000]);
          return;
        }
      }

      // Return the file
      ServletOutputStream out = res.getOutputStream();
      IO.copyFileB(file, out, 60 * 1000);
      /* try (WritableByteChannel cOut = Channels.newChannel(out)) {
        IO.copyFileWithChannels(file, cOut);
        res.flushBuffer();
      } */
    }

    // @todo Split up this exception handling: those from file access vs those from dealing with response
    //       File access: catch and res.sendError()
    //       response: don't catch (let bubble up out of doGet() etc)
    catch (FileNotFoundException e) {
      log.error("returnFile(): FileNotFoundException= " + filename);
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_NOT_FOUND);
    } catch (java.net.SocketException e) {
      log.info("returnFile(): SocketException sending file: " + filename + " " + e.getMessage());
    } catch (IOException e) {
      String eName = e.getClass().getName(); // dont want compile time dependency on ClientAbortException
      if (eName.equals("org.apache.catalina.connector.ClientAbortException")) {
        log.debug("returnFile(): ClientAbortException while sending file: " + filename + " " + e.getMessage());
        return;
      }

      if (e.getMessage().startsWith("File transfer not complete")) { // coming from FileTransfer.transferTo()
        log.debug("returnFile() "+e.getMessage());
        return;
      }

      log.error("returnFile(): IOException (" + e.getClass().getName() + ") sending file ", e);
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_NOT_FOUND, "Problem sending file: " + e.getMessage());
    }
  }
Write a file to the response stream. Handles Range requests.

@param req         request
@param res         response
@param file        must exist and not be a directory
@param contentType must not be null
@throws IOException or error