2929package org .janelia .saalfeldlab .n5 ;
3030
3131import org .apache .commons .io .IOUtils ;
32+
3233import org .apache .commons .lang3 .function .TriFunction ;
3334import org .janelia .saalfeldlab .n5 .N5Exception .N5IOException ;
3435import org .janelia .saalfeldlab .n5 .http .ListResponseParser ;
36+ import org .janelia .saalfeldlab .n5 .readdata .ReadData ;
3537
3638import java .io .Closeable ;
3739import java .io .IOException ;
5759 */
5860public class HttpKeyValueAccess implements KeyValueAccess {
5961
62+ public static final String HEAD = "HEAD" ;
63+ public static final String GET = "GET" ;
64+
65+ public static final String RANGE = "Range" ;
66+ public static final String ACCEPT_RANGE = "Accept-Range" ;
67+ public static final String BYTES = "bytes" ;
68+
6069 private int readTimeoutMilliseconds ;
6170 private int connectionTimeoutMilliseconds ;
6271
@@ -127,6 +136,12 @@ public boolean exists(final String normalPath) {
127136 }
128137 }
129138
139+ @ Override public long size (String normalPath ) {
140+
141+ final HttpURLConnection head = requireValidHttpResponse (normalPath , "HEAD" , "Error checking existence: " + normalPath , true );
142+ return head .getContentLengthLong ();
143+ }
144+
130145 /**
131146 * Test whether the path is a directory.
132147 * <p>
@@ -142,7 +157,7 @@ public boolean exists(final String normalPath) {
142157 public boolean isDirectory (final String normalPath ) {
143158
144159 try {
145- requireValidHttpResponse (getDirectoryPath (normalPath ), " HEAD" , (code , msg ,http ) -> {
160+ requireValidHttpResponse (getDirectoryPath (normalPath ), HEAD , (code , msg ,http ) -> {
146161 final N5Exception cause = validExistsResponse (code , "Error checking directory: " + normalPath , msg , true );
147162 if (code >= 300 && code < 400 ) {
148163 final String redirectLocation = http .getHeaderField ("Location" );
@@ -184,7 +199,7 @@ public boolean isFile(final String normalPath) {
184199
185200 /* Files must not end in `/` And Don't accept a redirect to a location ending in `/` */
186201 try {
187- requireValidHttpResponse (getFilePath (normalPath ), " HEAD" , (code , msg , http ) -> {
202+ requireValidHttpResponse (getFilePath (normalPath ), HEAD , (code , msg , http ) -> {
188203 final N5Exception cause = validExistsResponse (code , "Error accessing file: " + normalPath , msg , true );
189204 if (code >= 300 && code < 400 ) {
190205 final String redirectLocation = http .getHeaderField ("Location" );
@@ -216,12 +231,16 @@ private HttpURLConnection httpRequest(String normalPath, String method) throws I
216231 }
217232
218233 @ Override
234+ public ReadData createReadData (final String normalPath ) {
235+ return new KeyValueAccessReadData (new HttpLazyRead (normalPath ));
236+ }
237+
219238 public LockedChannel lockForReading (final String normalPath ) throws N5IOException {
220239 //TODO Caleb: Maybe check exists lazily when attempting to read
221240 try {
222241 if (!exists (normalPath ))
223242 throw new N5Exception .N5NoSuchKeyException ("Key does not exist: " + normalPath );
224- return new HttpObjectChannel (uri (normalPath ));
243+ return new HttpObjectChannel (uri (normalPath ), 0 , - 1 );
225244 } catch (URISyntaxException e ) {
226245 throw new N5Exception ("Invalid URI Syntax" , e );
227246 }
@@ -272,7 +291,7 @@ public String[] list(final String normalPath) throws N5IOException {
272291
273292 private String [] queryListEntries (String normalPath , ListResponseParser parser , boolean allowRedirect ) throws N5IOException {
274293
275- final HttpURLConnection http = requireValidHttpResponse (normalPath , " GET" , "Error listing directory at " + normalPath , allowRedirect );
294+ final HttpURLConnection http = requireValidHttpResponse (normalPath , GET , "Error listing directory at " + normalPath , allowRedirect );
276295 try {
277296 final String listResponse = responseToString (http .getInputStream ());
278297 return parser .parseListResponse (listResponse );
@@ -333,23 +352,47 @@ public void delete(final String normalPath) {
333352 private class HttpObjectChannel implements LockedChannel {
334353
335354 protected final URI uri ;
355+ private final long startByte ;
356+ private final long size ;
336357 private final ArrayList <Closeable > resources = new ArrayList <>();
337358
338- protected HttpObjectChannel (final URI uri ) {
359+ protected HttpObjectChannel (final URI uri , long startByte , long size ) {
339360
340361 this .uri = uri ;
362+ this .startByte = startByte ;
363+ this .size = size ;
364+ }
365+
366+ private boolean isPartialRead () {
367+ return startByte > 0 || (size >= 0 && size != Long .MAX_VALUE );
341368 }
342369
343370 @ Override
344371 public InputStream newInputStream () throws N5IOException {
345372
346373 try {
347- return uri .toURL ().openStream ();
374+ HttpURLConnection conn = (HttpURLConnection )uri .toURL ().openConnection ();
375+ if (isPartialRead ()) {
376+ conn .setRequestProperty (RANGE , rangeString ());
377+ final String acceptRanges = conn .getHeaderField (ACCEPT_RANGE );
378+ if (acceptRanges == null || !acceptRanges .equals (BYTES )) {
379+ conn .disconnect ();
380+ conn = (HttpURLConnection )uri .toURL ().openConnection ();
381+ return ReadData .from (conn .getInputStream ()).materialize ().slice (startByte , size ).inputStream ();
382+ }
383+ }
384+ return conn .getInputStream ();
348385 } catch (IOException e ) {
349386 throw new N5IOException ("Could not open stream for " + uri , e );
350387 }
351388 }
352389
390+ private String rangeString () {
391+
392+ final String lastByte = (size > 0 ) ? Long .toString (startByte + size - 1 ) : "" ;
393+ return String .format ("%s=%d-%s" , BYTES , startByte , lastByte );
394+ }
395+
353396 @ Override
354397 public Reader newReader () throws N5IOException {
355398
@@ -384,4 +427,29 @@ public void close() throws IOException {
384427 }
385428 }
386429
430+ private class HttpLazyRead implements LazyRead {
431+
432+ private final String normalKey ;
433+
434+ HttpLazyRead (String normalKey ) {
435+ this .normalKey = normalKey ;
436+ }
437+
438+ @ Override
439+ public long size () {
440+ return HttpKeyValueAccess .this .size (normalKey );
441+ }
442+
443+ @ Override
444+ public ReadData materialize (long offset , long length ) {
445+ try (final HttpObjectChannel ch = new HttpObjectChannel (uri (normalKey ), offset , length )) {
446+ return ReadData .from (ch .newInputStream ()).materialize ();
447+ } catch (IOException e ) {
448+ throw new N5IOException (e );
449+ } catch (URISyntaxException e ) {
450+ throw new N5Exception (e );
451+ }
452+ }
453+ }
454+
387455}
0 commit comments