mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-14 17:58:59 +00:00
ALF-17387: Support For HTTP Range Requests in Repository WebScripts
- Added HttpRangeProcessor.processRange which takes a WebScriptResponse parameter instead of HttpServletResponse - Changed HttpRangeProcessor.processSingeRange and HttpRangeProcessor.processMultiRange to accept a generic Object parameter then cast to the appropriate WebScriptResponse or HttpServletResponse - Added Javadoc to HttpRangeProcessor.processRange - Changed StreamContent.streamContentImpl to add code from BaseDownloadContentServlet which does the work of processing the range header from the request - Changed StreamContent.streamContentImpl method signature to accept nodeRef and propertyQName parameters needed for multi-range requests - Modified methods which override or call StreamContent.streamContentImpl for new method signature, passing in nodeRef and propertyQName or nulls where appropriate git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@45222 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -63,7 +63,9 @@ public class ContentInfo extends StreamContent
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, ContentReader reader, boolean attach, Date modified, String eTag, String attachFileName)
|
||||
protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res,
|
||||
ContentReader reader, NodeRef nodeRef, QName propertyQName,
|
||||
boolean attach, Date modified, String eTag, String attachFileName)
|
||||
throws IOException
|
||||
{
|
||||
setAttachment(res, attach, attachFileName);
|
||||
|
@@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.content.MimetypeMap;
|
||||
import org.alfresco.repo.content.filestore.FileContentReader;
|
||||
import org.alfresco.repo.web.util.HttpRangeProcessor;
|
||||
import org.alfresco.repo.webdav.WebDAVHelper;
|
||||
import org.alfresco.service.cmr.repository.ContentIOException;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
@@ -75,6 +76,12 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
* format definied by RFC 822, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3
|
||||
*/
|
||||
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US);
|
||||
|
||||
private static final String HEADER_CONTENT_RANGE = "Content-Range";
|
||||
private static final String HEADER_CONTENT_LENGTH = "Content-Length";
|
||||
private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges";
|
||||
private static final String HEADER_RANGE = "Range";
|
||||
private static final String HEADER_USER_AGENT = "User-Agent";
|
||||
|
||||
/** Services */
|
||||
protected PermissionService permissionService;
|
||||
@@ -430,7 +437,7 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
}
|
||||
|
||||
// Stream the content
|
||||
streamContentImpl(req, res, reader, attach, modified, modified == null ? null : String.valueOf(modified.getTime()), attachFileName, model);
|
||||
streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified, modified == null ? null : String.valueOf(modified.getTime()), attachFileName, model);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -668,7 +675,7 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
long lastModified = modifiedTime == null ? file.lastModified() : modifiedTime;
|
||||
Date lastModifiedDate = new Date(lastModified);
|
||||
|
||||
streamContentImpl(req, res, reader, attach, lastModifiedDate, String.valueOf(lastModifiedDate.getTime()), attachFileName, model);
|
||||
streamContentImpl(req, res, reader, null, null, attach, lastModifiedDate, String.valueOf(lastModifiedDate.getTime()), attachFileName, model);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -677,6 +684,8 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
* @param req The request
|
||||
* @param res The response
|
||||
* @param reader The reader
|
||||
* @param nodeRef The content nodeRef if applicable
|
||||
* @param propertyQName The content property if applicable
|
||||
* @param attach Indicates whether the content should be streamed as an attachment or not
|
||||
* @param modified Modified date of content
|
||||
* @param eTag ETag to use
|
||||
@@ -686,12 +695,14 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
protected void streamContentImpl(WebScriptRequest req,
|
||||
WebScriptResponse res,
|
||||
ContentReader reader,
|
||||
NodeRef nodeRef,
|
||||
QName propertyQName,
|
||||
boolean attach,
|
||||
Date modified,
|
||||
String eTag,
|
||||
String attachFileName) throws IOException
|
||||
{
|
||||
streamContentImpl(req, res, reader, attach, modified, eTag, attachFileName, null);
|
||||
streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified, eTag, attachFileName, null);
|
||||
}
|
||||
/**
|
||||
* Stream content implementation
|
||||
@@ -699,6 +710,8 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
* @param req The request
|
||||
* @param res The response
|
||||
* @param reader The reader
|
||||
* @param nodeRef The content nodeRef if applicable
|
||||
* @param propertyQName The content property if applicable
|
||||
* @param attach Indicates whether the content should be streamed as an attachment or not
|
||||
* @param modified Modified date of content
|
||||
* @param eTag ETag to use
|
||||
@@ -708,6 +721,8 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
protected void streamContentImpl(WebScriptRequest req,
|
||||
WebScriptResponse res,
|
||||
ContentReader reader,
|
||||
NodeRef nodeRef,
|
||||
QName propertyQName,
|
||||
boolean attach,
|
||||
Date modified,
|
||||
String eTag,
|
||||
@@ -730,20 +745,58 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
|
||||
}
|
||||
}
|
||||
|
||||
// set mimetype for the content and the character encoding + length for the stream
|
||||
res.setContentType(mimetype);
|
||||
res.setContentEncoding(reader.getEncoding());
|
||||
res.setHeader("Content-Length", Long.toString(reader.getSize()));
|
||||
|
||||
// set caching
|
||||
setResponseCache(res, modified, eTag, model);
|
||||
|
||||
// get the content and stream directly to the response output stream
|
||||
// assuming the repository is capable of streaming in chunks, this should allow large files
|
||||
// to be streamed directly to the browser response stream.
|
||||
res.setHeader(HEADER_ACCEPT_RANGES, "bytes");
|
||||
try
|
||||
{
|
||||
reader.getContent(res.getOutputStream());
|
||||
boolean processedRange = false;
|
||||
String range = req.getHeader(HEADER_CONTENT_RANGE);
|
||||
if (range == null)
|
||||
{
|
||||
range = req.getHeader(HEADER_RANGE);
|
||||
}
|
||||
if (range != null)
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
logger.debug("Found content range header: " + range);
|
||||
|
||||
// ensure the range header is starts with "bytes=" and process the range(s)
|
||||
if (range.length() > 6)
|
||||
{
|
||||
if (range.indexOf(',') != -1 && (nodeRef == null || propertyQName == null))
|
||||
{
|
||||
if (logger.isInfoEnabled())
|
||||
logger.info("Multi-range only supported for nodeRefs");
|
||||
}
|
||||
else {
|
||||
HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService);
|
||||
processedRange = rangeProcessor.processRange(
|
||||
res, reader, range.substring(6), nodeRef, propertyQName,
|
||||
mimetype, req.getHeader(HEADER_USER_AGENT));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (processedRange == false)
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
logger.debug("Sending complete file content...");
|
||||
|
||||
// set mimetype for the content and the character encoding for the stream
|
||||
res.setContentType(mimetype);
|
||||
res.setContentEncoding(reader.getEncoding());
|
||||
|
||||
// return the complete entity range
|
||||
long size = reader.getSize();
|
||||
res.setHeader(HEADER_CONTENT_RANGE, "bytes 0-" + Long.toString(size-1L) + "/" + Long.toString(size));
|
||||
res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size));
|
||||
|
||||
// set caching
|
||||
setResponseCache(res, modified, eTag, model);
|
||||
|
||||
// get the content and stream directly to the response output stream
|
||||
// assuming the repository is capable of streaming in chunks, this should allow large files
|
||||
// to be streamed directly to the browser response stream.
|
||||
reader.getContent( res.getOutputStream() );
|
||||
}
|
||||
}
|
||||
catch (SocketException e1)
|
||||
{
|
||||
|
@@ -227,7 +227,7 @@ public class NodeContentGet extends StreamContent
|
||||
else
|
||||
{
|
||||
res.setStatus(HttpStatus.SC_OK);
|
||||
streamContentImpl(req, res, textReader, false, modified, String.valueOf(modified.getTime()), null);
|
||||
streamContentImpl(req, res, textReader, null, null, false, modified, String.valueOf(modified.getTime()), null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
@@ -20,6 +20,7 @@ package org.alfresco.repo.web.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -34,6 +35,7 @@ import org.alfresco.service.cmr.repository.NodeRef;
|
||||
import org.alfresco.service.namespace.QName;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.extensions.webscripts.WebScriptResponse;
|
||||
|
||||
/**
|
||||
* Generates HTTP response for "Range" scoped HTTP requests for content.
|
||||
@@ -64,7 +66,17 @@ public class HttpRangeProcessor
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a range header - handles single and multiple range requests.
|
||||
* Process a range header for a HttpServletResponse - handles single and multiple range requests.
|
||||
*
|
||||
* @param res the HTTP servlet response
|
||||
* @param reader the content reader
|
||||
* @param range the byte range
|
||||
* @param ref the content NodeRef
|
||||
* @param property the content property
|
||||
* @param mimetype the content mimetype
|
||||
* @param userAgent the user agent string
|
||||
* @return whether or not the range could be processed
|
||||
* @throws IOException
|
||||
*/
|
||||
public boolean processRange(HttpServletResponse res, ContentReader reader, String range,
|
||||
NodeRef ref, QName property, String mimetype, String userAgent)
|
||||
@@ -80,6 +92,34 @@ public class HttpRangeProcessor
|
||||
return processMultiRange(res, range, ref, property, mimetype, userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a range header for a WebScriptResponse - handles single and multiple range requests.
|
||||
*
|
||||
* @param res the webscript response
|
||||
* @param reader the content reader
|
||||
* @param range the byte range
|
||||
* @param ref the content NodeRef
|
||||
* @param property the content property
|
||||
* @param mimetype the content mimetype
|
||||
* @param userAgent the user agent string
|
||||
* @return whether or not the range could be processed
|
||||
* @throws IOException
|
||||
*/
|
||||
public boolean processRange(WebScriptResponse res, ContentReader reader, String range,
|
||||
NodeRef ref, QName property, String mimetype, String userAgent)
|
||||
throws IOException
|
||||
{
|
||||
// test for multiple byte ranges present in header
|
||||
if (range.indexOf(',') == -1)
|
||||
{
|
||||
return processSingleRange(res, reader, range, mimetype);
|
||||
}
|
||||
else
|
||||
{
|
||||
return processMultiRange(res, range, ref, property, mimetype, userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single range request.
|
||||
@@ -91,9 +131,26 @@ public class HttpRangeProcessor
|
||||
*
|
||||
* @return true if processed range, false otherwise
|
||||
*/
|
||||
private boolean processSingleRange(HttpServletResponse res, ContentReader reader, String range, String mimetype)
|
||||
private boolean processSingleRange(Object res, ContentReader reader, String range, String mimetype)
|
||||
throws IOException
|
||||
{
|
||||
// Handle either HttpServletResponse or WebScriptResponse
|
||||
HttpServletResponse httpServletResponse = null;
|
||||
WebScriptResponse webScriptResponse = null;
|
||||
if (res instanceof HttpServletResponse)
|
||||
{
|
||||
httpServletResponse = (HttpServletResponse) res;
|
||||
}
|
||||
else if (res instanceof WebScriptResponse)
|
||||
{
|
||||
webScriptResponse = (WebScriptResponse) res;
|
||||
}
|
||||
if (httpServletResponse == null && webScriptResponse == null)
|
||||
{
|
||||
// Unknown response object type
|
||||
return false;
|
||||
}
|
||||
|
||||
// return the specific set of bytes as requested in the content-range header
|
||||
|
||||
/* Examples of byte-content-range-spec values, assuming that the entity contains total of 1234 bytes:
|
||||
@@ -117,18 +174,38 @@ public class HttpRangeProcessor
|
||||
if (getLogger().isDebugEnabled())
|
||||
getLogger().debug("Failed to parse range header - returning 416 status code: " + err.getMessage());
|
||||
|
||||
res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
res.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
|
||||
res.getOutputStream().close();
|
||||
if (httpServletResponse != null)
|
||||
{
|
||||
httpServletResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
httpServletResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
|
||||
httpServletResponse.getOutputStream().close();
|
||||
}
|
||||
else if (webScriptResponse != null)
|
||||
{
|
||||
webScriptResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
webScriptResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
|
||||
webScriptResponse.getOutputStream().close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// set Partial Content status and range headers
|
||||
res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
res.setContentType(mimetype);
|
||||
String contentRange = "bytes " + Long.toString(r.start) + "-" + Long.toString(r.end) + "/" + Long.toString(reader.getSize());
|
||||
res.setHeader(HEADER_CONTENT_RANGE, contentRange);
|
||||
res.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L));
|
||||
String contentRange = "bytes " + Long.toString(r.start) +
|
||||
"-" + Long.toString(r.end) + "/" + Long.toString(reader.getSize());
|
||||
if (httpServletResponse != null)
|
||||
{
|
||||
httpServletResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
httpServletResponse.setContentType(mimetype);
|
||||
httpServletResponse.setHeader(HEADER_CONTENT_RANGE, contentRange);
|
||||
httpServletResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L));
|
||||
}
|
||||
else if (webScriptResponse != null)
|
||||
{
|
||||
webScriptResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
webScriptResponse.setContentType(mimetype);
|
||||
webScriptResponse.setHeader(HEADER_CONTENT_RANGE, contentRange);
|
||||
webScriptResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L));
|
||||
}
|
||||
|
||||
if (getLogger().isDebugEnabled())
|
||||
getLogger().debug("Processing: Content-Range: " + contentRange);
|
||||
@@ -137,7 +214,15 @@ public class HttpRangeProcessor
|
||||
try
|
||||
{
|
||||
// output the binary data for the range
|
||||
ServletOutputStream os = res.getOutputStream();
|
||||
OutputStream os = null;
|
||||
if (httpServletResponse != null)
|
||||
{
|
||||
os = httpServletResponse.getOutputStream();
|
||||
}
|
||||
else if (webScriptResponse != null)
|
||||
{
|
||||
os = webScriptResponse.getOutputStream();
|
||||
}
|
||||
is = reader.getContentInputStream();
|
||||
|
||||
streamRangeBytes(r, is, os, 0L);
|
||||
@@ -172,11 +257,28 @@ public class HttpRangeProcessor
|
||||
* @return true if processed range, false otherwise
|
||||
*/
|
||||
private boolean processMultiRange(
|
||||
HttpServletResponse res, String range, NodeRef ref, QName property, String mimetype, String userAgent)
|
||||
Object res, String range, NodeRef ref, QName property, String mimetype, String userAgent)
|
||||
throws IOException
|
||||
{
|
||||
final Log logger = getLogger();
|
||||
|
||||
// Handle either HttpServletResponse or WebScriptResponse
|
||||
HttpServletResponse httpServletResponse = null;
|
||||
WebScriptResponse webScriptResponse = null;
|
||||
if (res instanceof HttpServletResponse)
|
||||
{
|
||||
httpServletResponse = (HttpServletResponse) res;
|
||||
}
|
||||
else if (res instanceof WebScriptResponse)
|
||||
{
|
||||
webScriptResponse = (WebScriptResponse) res;
|
||||
}
|
||||
if (httpServletResponse == null && webScriptResponse == null)
|
||||
{
|
||||
// Unknown response object type
|
||||
return false;
|
||||
}
|
||||
|
||||
// return the sets of bytes as requested in the content-range header
|
||||
// the response will be formatted as multipart/byteranges media type message
|
||||
|
||||
@@ -211,9 +313,18 @@ public class HttpRangeProcessor
|
||||
if (getLogger().isDebugEnabled())
|
||||
getLogger().debug("Failed to parse range header - returning 416 status code: " + err.getMessage());
|
||||
|
||||
res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
res.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
|
||||
res.getOutputStream().close();
|
||||
if (httpServletResponse != null)
|
||||
{
|
||||
httpServletResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
httpServletResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
|
||||
httpServletResponse.getOutputStream().close();
|
||||
}
|
||||
else if (webScriptResponse != null)
|
||||
{
|
||||
webScriptResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
webScriptResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
|
||||
webScriptResponse.getOutputStream().close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -257,11 +368,21 @@ public class HttpRangeProcessor
|
||||
}
|
||||
|
||||
// output headers as we have at least one range to process
|
||||
res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
res.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER);
|
||||
res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length));
|
||||
|
||||
ServletOutputStream os = res.getOutputStream();
|
||||
OutputStream os = null;
|
||||
if (httpServletResponse != null)
|
||||
{
|
||||
httpServletResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
httpServletResponse.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER);
|
||||
httpServletResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length));
|
||||
os = httpServletResponse.getOutputStream();
|
||||
}
|
||||
else if (webScriptResponse != null)
|
||||
{
|
||||
webScriptResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
webScriptResponse.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER);
|
||||
webScriptResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length));
|
||||
os =webScriptResponse.getOutputStream();
|
||||
}
|
||||
|
||||
InputStream is = null;
|
||||
try
|
||||
@@ -274,7 +395,8 @@ public class HttpRangeProcessor
|
||||
try
|
||||
{
|
||||
// output the header bytes for the range
|
||||
r.outputHeader(os);
|
||||
if (os instanceof ServletOutputStream)
|
||||
r.outputHeader((ServletOutputStream) os);
|
||||
|
||||
// output the binary data for the range
|
||||
// need a new reader for each new InputStream
|
||||
@@ -284,7 +406,8 @@ public class HttpRangeProcessor
|
||||
is = null;
|
||||
|
||||
// section marker and flush stream
|
||||
os.println();
|
||||
if (os instanceof ServletOutputStream)
|
||||
((ServletOutputStream) os).println();
|
||||
os.flush();
|
||||
}
|
||||
catch (IOException err)
|
||||
@@ -304,7 +427,8 @@ public class HttpRangeProcessor
|
||||
}
|
||||
|
||||
// end marker
|
||||
os.println(MULTIPART_BYTERANGES_BOUNDRY_END);
|
||||
if (os instanceof ServletOutputStream)
|
||||
((ServletOutputStream) os).println(MULTIPART_BYTERANGES_BOUNDRY_END);
|
||||
os.close();
|
||||
processedRange = true;
|
||||
}
|
||||
@@ -322,7 +446,7 @@ public class HttpRangeProcessor
|
||||
*
|
||||
* @return current InputStream position - so the stream can be reused if required
|
||||
*/
|
||||
private void streamRangeBytes(final Range r, final InputStream is, final ServletOutputStream os, long offset)
|
||||
private void streamRangeBytes(final Range r, final InputStream is, final OutputStream os, long offset)
|
||||
throws IOException
|
||||
{
|
||||
final Log logger = getLogger();
|
||||
@@ -496,7 +620,6 @@ public class HttpRangeProcessor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the logger
|
||||
*/
|
||||
|
Reference in New Issue
Block a user