11package cwms .cda .api ;
22
3+ import com .google .common .flogger .FluentLogger ;
34import io .javalin .core .util .Header ;
45import io .javalin .http .Context ;
56import java .io .IOException ;
67import java .io .InputStream ;
78import java .io .OutputStream ;
8- import java .util .Arrays ;
99import java .util .List ;
10+ import org .apache .commons .io .IOUtils ;
1011
1112public class RangeRequestUtil {
13+ static FluentLogger logger = FluentLogger .forEnclosingClass ();
1214
1315 private RangeRequestUtil () {
1416 // utility class
@@ -19,78 +21,79 @@ private RangeRequestUtil() {
1921 * take the InputStream, wrap it in a CompletedFuture and then process the request asynchronously. This
2022 * causes problems when the InputStream is tied to a database connection that gets closed before the
2123 * async processing happens. This method doesn't do the async thing but tries to support the rest.
22- * @param ctx
23- * @param is
24- * @param mediaType
25- * @param totalBytes
26- * @throws IOException
24+ * @param ctx the Javalin context
25+ * @param is the input stream
26+ * @param mediaType the content type
27+ * @param totalBytes the total number of bytes in the input stream
28+ * @throws IOException if either of the streams throw an IOException
2729 */
2830 public static void seekableStream (Context ctx , InputStream is , String mediaType , long totalBytes ) throws IOException {
29- long from = 0 ;
30- long to = totalBytes - 1 ;
31+
3132 if (ctx .header (Header .RANGE ) == null ) {
33+ // Not a range request.
3234 ctx .res .setContentType (mediaType );
35+
3336 // Javalin's version of this method doesn't set the content-length
3437 // Not setting the content-length makes the servlet container use Transfer-Encoding=chunked.
3538 // Chunked is a worse experience overall, seems like we should just set the length if we know it.
36- writeRange (ctx .res .getOutputStream (), is , from , Math .min (to , totalBytes - 1 ));
39+ ctx .header (Header .CONTENT_LENGTH , String .valueOf (totalBytes ));
40+
41+ IOUtils .copyLarge (is , (OutputStream ) ctx .res .getOutputStream (), 0 , totalBytes );
3742 } else {
38- int chunkSize = 128000 ;
3943 String rangeHeader = ctx .header (Header .RANGE );
40- String [] eqSplit = rangeHeader .split ("=" , 2 );
41- String [] dashSplit = eqSplit [1 ].split ("-" , -1 ); // keep empty trailing part
42-
43- List <String > requestedRange = Arrays .stream (dashSplit )
44- .filter (s -> !s .isEmpty ())
45- .collect (java .util .stream .Collectors .toList ());
46-
47- from = Long .parseLong (requestedRange .get (0 ));
4844
49- if (from + chunkSize > totalBytes ) {
50- // chunk bigger than file, write all
51- to = totalBytes - 1 ;
52- } else if (requestedRange .size () == 2 ) {
53- // chunk smaller than file, to/from specified
54- to = Long .parseLong (requestedRange .get (1 ));
45+ List <long []> ranges = RangeParser .parse (rangeHeader );
46+
47+ long [] requestedRange = ranges .get (0 );
48+ if ( ranges .size () > 1 ){
49+ // we support range requests but we not currently supporting multiple ranges.
50+ // Range request are optional so we have choices what to do if multiple ranges are requested:
51+ // We could return 416 and hope the client figures out to only send one range
52+ // We could service the first range with 206 and ignore the other ranges
53+ // We could ignore the range request entirely and return the full body with 200
54+ // We could implement support for multiple ranges
55+ logger .atInfo ().log ("Multiple ranges requested, using first and ignoring additional ranges" );
5556 } else {
56- // chunk smaller than file, to/from not specified
57- to = from + chunkSize - 1 ;
58- }
57+ requestedRange = RangeParser .interpret (requestedRange , totalBytes );
5958
60- ctx .status (206 );
59+ long from = requestedRange [0 ];
60+ long to = requestedRange [1 ];
6161
62- ctx .header (Header .ACCEPT_RANGES , "bytes" );
63- ctx .header (Header .CONTENT_RANGE , "bytes " + from + "-" + to + "/" + totalBytes );
62+ ctx .status (206 );
6463
65- ctx .res .setContentType (mediaType );
66- ctx .header (Header .CONTENT_LENGTH , String .valueOf (Math .min (to - from + 1 , totalBytes )));
67- writeRange (ctx .res .getOutputStream (), is , from , Math .min (to , totalBytes - 1 ));
64+ ctx .header (Header .ACCEPT_RANGES , "bytes" );
65+ ctx .header (Header .CONTENT_RANGE , "bytes " + from + "-" + to + "/" + totalBytes );
66+
67+ ctx .res .setContentType (mediaType );
68+ ctx .header (Header .CONTENT_LENGTH , String .valueOf (Math .min (to - from + 1 , totalBytes )));
69+ writeRange (ctx .res .getOutputStream (), is , from , Math .min (to , totalBytes - 1 ));
70+ }
6871 }
6972 }
7073
71-
74+ /**
75+ * Writes a range of bytes from the input stream to the output stream.
76+ * @param out the output stream to write to.
77+ * @param in the input stream to read from. It is assumed that this stream is open and positioned at 0.
78+ * @param from the starting byte position to read from (inclusive)
79+ * @param to the ending byte position to read to (inclusive)
80+ * @throws IOException if either of the streams throw an IOException
81+ */
7282 public static void writeRange (OutputStream out , InputStream in , long from , long to ) throws IOException {
73- writeRange (out , in , from , to , new byte [8192 ]);
83+ skip (in , from );
84+ long len = to - from + 1 ;
85+
86+ // If the inputOffset to IOUtils.copyLarge is not 0 then IOUtils will do its own skipping. For reasons
87+ // that IOUtils explains (quirks of certain streams) it does its skipping via read(). Using read() has performance
88+ // implications b/c all the skipped data gets copied to memory. We do our own skipping and then have IOUtils copy.
89+ IOUtils .copyLarge (in , out , 0 , len );
7490 }
7591
76- public static void writeRange (OutputStream out , InputStream is , long from , long to , byte [] buffer ) throws IOException {
77- long toSkip = from ;
92+ private static void skip (InputStream is , long toSkip ) throws IOException {
7893 while (toSkip > 0 ) {
7994 long skipped = is .skip (toSkip );
8095 toSkip -= skipped ;
8196 }
82-
83- long bytesLeft = to - from + 1 ;
84- while (bytesLeft != 0L ) {
85- int maxRead = (int ) Math .min (buffer .length , bytesLeft );
86- int read = is .read (buffer , 0 , maxRead );
87- if (read == -1 ) {
88- break ;
89- }
90- out .write (buffer , 0 , read );
91- bytesLeft -= read ;
92- }
93-
9497 }
9598
9699}
0 commit comments