/* * This file is part of the Wayback archival access software * (http://archive-access.sourceforge.net/projects/wayback/). * * Licensed to the Internet Archive (IA) by one or more individual * contributors. * * The IA licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.archive.wayback.replay; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletResponse; import org.archive.wayback.core.Resource; import org.archive.wayback.exception.RangeNotSatisfiableException; import com.google.common.io.ByteStreams; /** * RangeResource decorates Resource to render partial content response * to range request. * * After constructing with wrapped Resource and requested range, * call {@link #parseRange()} to prepare internal state for rendering. * The method will throw {@link RangeNotSatisfiableException} if the requested * byte ranges cannot be replayed with the base Resource. * * RangeResource currently only works with: * <ul> * <li>A single-range request * <li>A 200 capture with {@code Content-Length}, or a valid 206 capture * with single range (i.e. no support for {@code multipart/byteranges}) * </ul> * @see HttpHeaderOperation#parseRanges(String) */ public class RangeResource extends Resource { private Resource origResource; private long[][] requestedRanges; private long outputLength; private Map<String, String> httpHeaders; /** * Initialize with base Resource and requested ranges. * @param origResource base resource * @param requestedRanges requested range * @see HttpHeaderOperation#parseRanges(String) */ public RangeResource(Resource origResource, long[][] requestedRanges) { this.origResource = origResource; this.requestedRanges = requestedRanges; } /** * Prepare response for requested range. * @throws RangeNotSatisfiableException base Resource either does not have * enough data to fulfill the request, or have invalid header field. */ public void parseRange() throws RangeNotSatisfiableException, IOException { if (requestedRanges.length > 1) { throw new RangeNotSatisfiableException(origResource, requestedRanges, "Multiple ranges are not supported yet"); } final long[] firstRange = requestedRanges[0]; long start = firstRange[0]; long stop = firstRange[1]; long[] availRange = availableRange(); if (availRange == null) { throw new RangeNotSatisfiableException(origResource, requestedRanges, "available range cannot be determined"); } if (start < 0) { // tail range (-N) - translate to absolute positions start = availRange[2] + start; stop = availRange[2]; } if (stop < 0) { // M- stop = availRange[2]; } if (start < availRange[0] || stop > availRange[1]) { // requested range is not satisfiable with content available // in the resource. // TODO: if availRange[1] == file length, stop > availRange[1] // is okay. We just replay start..availRange[1]. Guessing it // must be extremely rare to have such capture. throw new RangeNotSatisfiableException(origResource, requestedRanges, "requested range is not available in this capture"); } if (start > availRange[0]) { origResource.skip(start - availRange[0]); } outputLength = stop - start; setInputStream(ByteStreams.limit(origResource, outputLength)); httpHeaders = new HashMap<String, String>(); httpHeaders.putAll(origResource.getHttpHeaders()); // TODO: Add Content-Range String contentRange = String.format("bytes %d-%d/%d", start, stop - 1, availRange[2]); HttpHeaderOperation.replaceHeader(httpHeaders, HttpHeaderOperation.HTTP_CONTENT_RANGE_HEADER, contentRange); HttpHeaderOperation .replaceHeader(httpHeaders, HttpHeaderOperation.HTTP_LENGTH_HEADER, Long.toString(stop - start)); } /** * Look at resource HTTP header fields and determine the byte range available. * @return array with three long values: * <ol> * <li><em>offset of first available byte</em>, * <li><em>offset of last available byte</em> + 1, * <li><em>instance-length</em> * </ol> * Note this method returns {@code null} for {@code multipart/byteranges} * response. */ protected long[] availableRange() { // for revisit capture, HTTP headers comes from revisiting resource. // it should be okay to assume revisited resource has exactly the same // Content-Range header field. Map<String, String> respHeaders = origResource.getHttpHeaders(); long[] contentRange = HttpHeaderOperation .getContentRange(respHeaders); long availStart, availStop, contentLength; if (contentRange == null) { // regular 200 response (or Content-Range is invalid) String clen = HttpHeaderOperation.getContentLength(respHeaders); if (clen != null) { try { contentLength = Long.parseLong(clen); } catch (NumberFormatException ex) { return null; } } else { // no Content-Length - probably chunked encoded - // not supported yet (TODO: determine length by // actually reading?) return null; } availStart = 0; availStop = contentLength; } else { // 206, or perhaps 416 response if (contentRange[0] < 0) { // 416 (both [0] and [1] are -1 for */instance-length) return null; } availStart = contentRange[0]; availStop = contentRange[1]; contentLength = contentRange[2]; // TODO: need support for M-N/*? if (contentLength < 0) return null; // sanity check if (availStop <= availStart) return null; if (contentLength < availStop) return null; } return new long[] { availStart, availStop, contentLength }; } @Override public void close() throws IOException { if (origResource != null) { origResource.close(); origResource = null; } } @Override public Map<String, String> getHttpHeaders() { return httpHeaders; } @Override public long getRecordLength() { return origResource.getRecordLength(); } @Override public int getStatusCode() { return HttpServletResponse.SC_PARTIAL_CONTENT; } }