code: package io.lakefs.storage;
  
  import java.io.EOFException;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.HttpURLConnection;
  import java.net.MalformedURLException;
  import java.net.URL;
  
  import org.apache.commons.io.IOUtils;
  import org.apache.hadoop.fs.FSExceptionMessages;
  import org.apache.hadoop.fs.FSInputStream;
  
  public class HttpRangeInputStream extends FSInputStream {
      private static final int DEFAULT_BUFFER_SIZE_BYTES = 1024 * 1024;
      private final String url;
      private final int bufferSize;
  
      private long start = Long.MAX_VALUE;
      private long pos;
      private long len = 0;
      private byte[] rangeContent;
  
      private boolean closed;
      
      public HttpRangeInputStream(String url) throws IOException {
          this(url, DEFAULT_BUFFER_SIZE_BYTES);
      }
  
      public HttpRangeInputStream(String url, int bufferSize) throws IOException {
          this.url = url;
          this.bufferSize = bufferSize;
          HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
          connection.setRequestMethod("GET");
          connection.setRequestProperty("Range", "bytes=0-0");
          String contentRangeHeader = connection.getHeaderField("Content-Range");
          if (contentRangeHeader == null || !contentRangeHeader.startsWith("bytes 0-0/")) {
              // empty file
              return;
          }
          len = Long.parseLong(contentRangeHeader.substring("bytes 0-0/".length()));
      }
  
      private void updateInputStream(long targetPos) throws MalformedURLException, IOException {
          if (targetPos >= start && targetPos < start + bufferSize) {
              // no need to update the stream
              return;
          }
          HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
          connection.setRequestMethod("GET");
          long rangeEnd = Math.min(targetPos + bufferSize, len);
          connection.setRequestProperty("Range", "bytes=" + targetPos + "-" + rangeEnd);
          rangeContent = new byte[(int) (rangeEnd - targetPos)];
          try (InputStream inputStream = connection.getInputStream()) {
              IOUtils.readFully(inputStream, rangeContent);
          }
          start = targetPos;
      }
  
      @Override
      public synchronized void seek(long targetPos) throws IOException {
          if (closed) {
              throw new IOException("Stream closed");
          }
          if (targetPos < 0) {
              throw new EOFException(FSExceptionMessages.NEGATIVE_SEEK
                      + " " + targetPos);
          }
          this.pos = targetPos;
      }
  
      @Override
      public synchronized long getPos() throws IOException {
          return pos;
      }
  
      public synchronized int available() throws IOException {
          if (closed) {
              throw new IOException("Stream closed");
          }
          if (len - pos > Integer.MAX_VALUE) {
              return Integer.MAX_VALUE;
          }
          return (int) Math.max(len - pos, 0);
      }
  
      @Override
      public synchronized boolean seekToNewSource(long targetPos) throws IOException {
          return false;
      }
  
      @Override
      public synchronized int read() throws IOException {
          if (closed) {
              throw new IOException("Stream closed");
          }
          if (pos >= len) {
              return -1;
          }
          updateInputStream(pos);
          int res = rangeContent[(int) (pos - start)] & 0xff;
          pos++;
          return res;
      }
  
      @Override
      public synchronized void close() throws IOException {
          if (closed) {
              return;
          }
          closed = true;
      }
  }
  