DS-3651: several improvements

This commit is contained in:
frederic
2017-10-09 15:26:45 +02:00
committed by Tom Desair
parent eb65a3fc73
commit db54bc12f2
11 changed files with 229 additions and 125 deletions

View File

@@ -7,11 +7,6 @@
*/ */
package org.dspace.content; package org.dspace.content;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamService; import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants; import org.dspace.core.Constants;
@@ -19,6 +14,10 @@ import org.dspace.core.Context;
import org.hibernate.proxy.HibernateProxyHelper; import org.hibernate.proxy.HibernateProxyHelper;
import javax.persistence.*; import javax.persistence.*;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/** /**
* Class representing bitstreams stored in the DSpace system. * Class representing bitstreams stored in the DSpace system.

View File

@@ -27,9 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Iterator; import java.util.*;
import java.util.List;
import java.util.UUID;
/** /**
* Service implementation for the Bitstream object. * Service implementation for the Bitstream object.
@@ -453,4 +451,8 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl<Bitstream> imp
public List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException { public List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException {
return bitstreamDAO.getNotReferencedBitstreams(context); return bitstreamDAO.getNotReferencedBitstreams(context);
} }
public String getLastModified(Bitstream bitstream) {
return bitstreamStorageService.getLastModified(bitstream);
}
} }

View File

@@ -202,4 +202,6 @@ public interface BitstreamService extends DSpaceObjectService<Bitstream>, DSpace
int countBitstreamsWithoutPolicy(Context context) throws SQLException; int countBitstreamsWithoutPolicy(Context context) throws SQLException;
List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException; List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLException;
public String getLastModified(Bitstream bitstream);
} }

View File

@@ -339,6 +339,17 @@ public class BitstreamStorageServiceImpl implements BitstreamStorageService, Ini
} }
} }
public String getLastModified(Bitstream bitstream) {
Map wantedMetadata = new HashMap();
wantedMetadata.put("modified", null);
try {
wantedMetadata = stores.get(incoming).about(bitstream, wantedMetadata);
} catch (IOException e) {
log.error(e);
}
return wantedMetadata.get("modified").toString();
}
/** /**
* *
* @param context * @param context

View File

@@ -190,4 +190,7 @@ public interface BitstreamStorageService {
*/ */
public void migrate(Context context, Integer assetstoreSource, Integer assetstoreDestination, boolean deleteOld, Integer batchCommitSize) throws IOException, SQLException, AuthorizeException; public void migrate(Context context, Integer assetstoreSource, Integer assetstoreDestination, boolean deleteOld, Integer batchCommitSize) throws IOException, SQLException, AuthorizeException;
public String getLastModified(Bitstream bitstream);
} }

View File

@@ -15,8 +15,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.tika.Tika;
import org.apache.tika.mime.MimeTypes;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.repository.BitstreamRestRepository; import org.dspace.app.rest.repository.BitstreamRestRepository;
import org.dspace.app.rest.utils.MultipartFileSender; import org.dspace.app.rest.utils.MultipartFileSender;
@@ -38,10 +36,7 @@ public class BitstreamContentRestController {
@Autowired @Autowired
private BitstreamRestRepository bitstreamRestRepository; private BitstreamRestRepository bitstreamRestRepository;
private int buffer = 20480; @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD})
@RequestMapping(method = RequestMethod.GET)
public void retrieve(@PathVariable UUID uuid, HttpServletResponse response, public void retrieve(@PathVariable UUID uuid, HttpServletResponse response,
HttpServletRequest request) throws IOException { HttpServletRequest request) throws IOException {
BitstreamRest bit = bitstreamRestRepository.findOne(uuid); BitstreamRest bit = bitstreamRestRepository.findOne(uuid);
@@ -53,24 +48,26 @@ public class BitstreamContentRestController {
// Pipe the bits // Pipe the bits
InputStream is = bitstreamRestRepository.retrieve(uuid); InputStream is = bitstreamRestRepository.retrieve(uuid);
long lastModified = bitstreamRestRepository.getLastModified(uuid);
String mimetype = bit.getFormat().getMimetype(); String mimetype = bit.getFormat().getMimetype();
//This should be improved somewhere else so we don't have to look for the correct mimetype here
if (mimetype.equals(MimeTypes.OCTET_STREAM)) { //TODO LOG DOWNLOAD if no range or if last chunk
Tika tika = new Tika();
mimetype = tika.detect(is);
is.close();
is = bitstreamRestRepository.retrieve(uuid);
}
//MultipartFileSender //MultipartFileSender
try { try {
MultipartFileSender.fromBitstream(bit).with(request).with(response).with(is).with(mimetype).serveResource(); MultipartFileSender.fromBitstream(bit).with(request).with(response).withInputStream(is).withMimetype(mimetype).withLastModified(lastModified).serveResource();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
IOUtils.closeQuietly(is); IOUtils.closeQuietly(is);
} }
} }
} }

View File

@@ -7,9 +7,6 @@
*/ */
package org.dspace.app.rest.converter; package org.dspace.app.rest.converter;
import java.sql.SQLException;
import java.util.List;
import org.dspace.app.rest.model.BitstreamFormatRest; import org.dspace.app.rest.model.BitstreamFormatRest;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.model.CheckSumRest; import org.dspace.app.rest.model.CheckSumRest;
@@ -17,6 +14,9 @@ import org.dspace.content.Bundle;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.util.List;
/** /**
* This is the converter from/to the Bitstream in the DSpace API data model and the REST data model * This is the converter from/to the Bitstream in the DSpace API data model and the REST data model

View File

@@ -110,4 +110,15 @@ public class BitstreamRestRepository extends DSpaceRestRepository<BitstreamRest,
context.abort(); context.abort();
return is; return is;
} }
public long getLastModified(UUID id) {
Bitstream bit = null;
Context context = obtainContext();
try {
bit = bs.find(context, id);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
return Long.valueOf(bs.getLastModified(bit));
}
} }

View File

@@ -2,6 +2,8 @@ package org.dspace.app.rest.utils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -11,34 +13,69 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.attribute.FileTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.List; import java.util.List;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
/** /**
* Created by kevin on 10/02/15. * Created by kevin on 10/02/15.
* See full code here : https://github.com/davinkevin/Podcast-Server/blob/d927d9b8cb9ea1268af74316cd20b7192ca92da7/src/main/java/lan/dk/podcastserver/utils/multipart/MultipartFileSender.java * See full code here : https://github.com/davinkevin/Podcast-Server/blob/d927d9b8cb9ea1268af74316cd20b7192ca92da7/src/main/java/lan/dk/podcastserver/utils/multipart/MultipartFileSender.java
*/ */
public class MultipartFileSender { public class MultipartFileSender {
protected final Logger logger = LoggerFactory.getLogger(this.getClass()); protected final Logger log = LoggerFactory.getLogger(this.getClass());
private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB. private static final String METHOD_HEAD = "HEAD";
private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES"; private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
private static final String CONTENT_TYPE_MULTITYPE_WITH_BOUNDARY = "multipart/byteranges; boundary=" + MULTIPART_BOUNDARY;
private static final String CONTENT_DISPOSITION_INLINE = "inline";
private static final String CONTENT_DISPOSITION_ATTACHMENT = "attachment";
private static final String IF_NONE_MATCH = "If-None-Match";
private static final String IF_MODIFIED_SINCE = "If-Modified-Since";
private static final String ETAG = "ETag";
private static final String IF_MATCH = "If-Match";
private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
private static final String RANGE = "Range";
private static final String CONTENT_RANGE = "Content-Range";
private static final String IF_RANGE = "If-Range";
private static final String CONTENT_TYPE = "Content-Type";
private static final String ACCEPT_RANGES = "Accept-Ranges";
private static final String BYTES = "bytes";
private static final String LAST_MODIFIED = "Last-Modified";
private static final String EXPIRES = "Expires";
private static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
private static final String IMAGE = "image";
private static final String ACCEPT = "Accept";
private static final String CONTENT_DISPOSITION = "Content-Disposition";
private static final String CONTENT_LENGTH = "Content-Length";
private static final String BYTES_RANGE_FORMAT = "bytes %d-%d/%d";
private static final String CONTENT_DISPOSITION_FORMAT = "%s;filename=\"%s\"";
private static final String BYTES_DINVALID_BYTE_RANGE_FORMAT = "bytes */%d";
private static final String CACHE_CONTROL = "Cache-Control";
private static int DEFAULT_BUFFER_SIZE = 1000000; // ..bytes = 20KB.
private static final long DEFAULT_EXPIRE_TIME = 60L * 60L * 1000L; // ..ms = 1 week.
//no-cache so request is always performed for logging
private static final String CACHE_CONTROL_SETTING = "private, no-cache";
BitstreamRest bitstream; BitstreamRest bitstream;
InputStream inputStream; InputStream inputStream;
HttpServletRequest request; HttpServletRequest request;
HttpServletResponse response; HttpServletResponse response;
String contentType; String contentType;
String disposition = CONTENT_DISPOSITION_INLINE;
long lastModified;
public MultipartFileSender() { public MultipartFileSender() {
ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
String bufferSize = configurationService.getProperty("bitstream-download.buffer.size");
if (StringUtils.isNotEmpty(bufferSize)) {
DEFAULT_BUFFER_SIZE = Integer.parseInt(bufferSize);
}
} }
public static MultipartFileSender fromBitstream(BitstreamRest bitstream) { public static MultipartFileSender fromBitstream(BitstreamRest bitstream) {
@@ -61,17 +98,23 @@ public class MultipartFileSender {
return this; return this;
} }
public MultipartFileSender with(InputStream inputStream) { public MultipartFileSender withInputStream(InputStream inputStream) {
this.inputStream = inputStream; this.inputStream = inputStream;
return this; return this;
} }
public MultipartFileSender with(String mimetype) { public MultipartFileSender withMimetype(String mimetype) {
this.contentType = mimetype; this.contentType = mimetype;
return this; return this;
} }
public MultipartFileSender withLastModified(long lastModified) {
this.lastModified = lastModified;
return this;
}
public void serveResource() throws Exception { public void serveResource() throws Exception {
if (response == null || request == null) { if (response == null || request == null) {
return; return;
} }
@@ -84,29 +127,28 @@ public class MultipartFileSender {
Long length = bitstream.getSizeBytes(); Long length = bitstream.getSizeBytes();
String fileName = bitstream.getName(); String fileName = bitstream.getName();
FileTime lastModifiedObj = FileTime.fromMillis(new Date().getTime());
if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) { if (StringUtils.isEmpty(fileName)) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return; return;
} }
long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
// Validate request headers for caching --------------------------------------------------- // Validate request headers for caching ---------------------------------------------------
// If-None-Match header should contain "*" or ETag. If so, then return 304. // If-None-Match header should contain "*" or ETag. If so, then return 304.
String ifNoneMatch = request.getHeader("If-None-Match"); String ifNoneMatch = request.getHeader(IF_NONE_MATCH);
if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) { // if (nonNull(ifNoneMatch) && matches(ifNoneMatch, bitstream.getCheckSum().getValue())) {
response.setHeader("ETag", fileName); // Required in 304. // log.error("If-None-Match header should contain \"*\" or ETag. If so, then return 304.");
response.sendError(HttpServletResponse.SC_NOT_MODIFIED); // response.setHeader(ETAG, bitstream.getCheckSum().getValue()); // Required in 304.
return; // response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
} // return;
// }
// If-Modified-Since header should be greater than LastModified. If so, then return 304. // If-Modified-Since header should be greater than LastModified. If so, then return 304.
// This header is ignored if any If-None-Match header is specified. // This header is ignored if any If-None-Match header is specified.
long ifModifiedSince = request.getDateHeader("If-Modified-Since"); long ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE);
if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) { if (isNull(ifNoneMatch) && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
response.setHeader("ETag", fileName); // Required in 304. log.error("If-Modified-Since header should be greater than LastModified. If so, then return 304.");
response.setHeader(ETAG, bitstream.getCheckSum().getValue()); // Required in 304.
response.sendError(HttpServletResponse.SC_NOT_MODIFIED); response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
return; return;
} }
@@ -114,15 +156,17 @@ public class MultipartFileSender {
// Validate request headers for resume ---------------------------------------------------- // Validate request headers for resume ----------------------------------------------------
// If-Match header should contain "*" or ETag. If not, then return 412. // If-Match header should contain "*" or ETag. If not, then return 412.
String ifMatch = request.getHeader("If-Match"); String ifMatch = request.getHeader(IF_MATCH);
if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) { if (nonNull(ifMatch) && !matches(ifMatch, fileName)) {
log.error("If-Match header should contain \"*\" or ETag. If not, then return 412.");
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return; return;
} }
// If-Unmodified-Since header should be greater than LastModified. If not, then return 412. // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since"); long ifUnmodifiedSince = request.getDateHeader(IF_UNMODIFIED_SINCE);
if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) { if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
log.error("If-Unmodified-Since header should be greater than LastModified. If not, then return 412.");
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return; return;
} }
@@ -134,20 +178,21 @@ public class MultipartFileSender {
List<Range> ranges = new ArrayList<>(); List<Range> ranges = new ArrayList<>();
// Validate and process Range and If-Range headers. // Validate and process Range and If-Range headers.
String range = request.getHeader("Range"); String range = request.getHeader(RANGE);
if (range != null) { if (nonNull(range)) {
// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416. // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416. log.error("Range header should match format \"bytes=n-n,n-n,n-n...\". If not, then return 416.");
response.setHeader(CONTENT_RANGE, String.format(BYTES_DINVALID_BYTE_RANGE_FORMAT, length)); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return; return;
} }
String ifRange = request.getHeader("If-Range"); String ifRange = request.getHeader(IF_RANGE);
if (ifRange != null && !ifRange.equals(fileName)) { if (nonNull(ifRange) && !ifRange.equals(fileName)) {
try { try {
long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid. long ifRangeTime = request.getDateHeader(IF_RANGE); // Throws IAE if invalid.
if (ifRangeTime != -1) { if (ifRangeTime != -1) {
ranges.add(full); ranges.add(full);
} }
@@ -158,6 +203,7 @@ public class MultipartFileSender {
// If any valid If-Range header, then process each part of byte range. // If any valid If-Range header, then process each part of byte range.
if (ranges.isEmpty()) { if (ranges.isEmpty()) {
log.info("If any valid If-Range header, then process each part of byte range.");
for (String part : range.substring(6).split(",")) { for (String part : range.substring(6).split(",")) {
// Assuming a file with length of 100, the following examples returns bytes at: // Assuming a file with length of 100, the following examples returns bytes at:
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100). // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
@@ -173,7 +219,8 @@ public class MultipartFileSender {
// Check if Range is syntactically valid. If not, then return 416. // Check if Range is syntactically valid. If not, then return 416.
if (start > end) { if (start > end) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416. log.info("Check if Range is syntactically valid. If not, then return 416.");
response.setHeader(CONTENT_RANGE, String.format(BYTES_DINVALID_BYTE_RANGE_FORMAT, length)); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return; return;
} }
@@ -184,57 +231,62 @@ public class MultipartFileSender {
} }
} }
// Prepare and initialize response -------------------------------------------------------- log.debug("Content-Type : {}", contentType);
// Get content type by file name and set content disposition.
String disposition = "inline";
// If content type is unknown, then set the default value.
// For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
// To add new content types, add new mime-mapping entry in web.xml.
if (contentType == null) {
contentType = "application/octet-stream";
} else if (!contentType.startsWith("image")) {
// Else, expect for images, determine content disposition. If content type is supported by
// the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
String accept = request.getHeader("Accept");
disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment";
}
logger.debug("Content-Type : {}", contentType);
// Initialize response. // Initialize response.
response.reset(); response.reset();
response.setBufferSize(DEFAULT_BUFFER_SIZE); response.setBufferSize(DEFAULT_BUFFER_SIZE);
response.setHeader("Content-Type", contentType); response.setHeader(CONTENT_TYPE, contentType);
response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\""); response.setHeader(ACCEPT_RANGES, BYTES);
logger.debug("Content-Disposition : {}", disposition); response.setHeader(ETAG, bitstream.getCheckSum().getValue());
response.setHeader("Accept-Ranges", "bytes"); response.setDateHeader(LAST_MODIFIED, lastModified);
response.setHeader("ETag", fileName); response.setDateHeader(EXPIRES, System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);
response.setDateHeader("Last-Modified", lastModified);
response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);
//TODO what does this mean
response.setHeader(CACHE_CONTROL, "private, no-cache");
if (isNullOrEmpty(disposition)) {
if (contentType == null) {
contentType = APPLICATION_OCTET_STREAM;
} else if (!contentType.startsWith(IMAGE)) {
String accept = request.getHeader(ACCEPT);
disposition = accept != null && accepts(accept, contentType) ? CONTENT_DISPOSITION_INLINE : CONTENT_DISPOSITION_ATTACHMENT;
}
response.setHeader(CONTENT_DISPOSITION, String.format(CONTENT_DISPOSITION_FORMAT, disposition, fileName));
log.debug("Content-Disposition : {}", disposition);
}
// Content phase
if (METHOD_HEAD.equals(request.getMethod())) {
log.debug("HEAD request - skipping content");
return;
}
// Send requested file (part(s)) to client ------------------------------------------------ // Send requested file (part(s)) to client ------------------------------------------------
// Prepare streams. // Prepare streams.
try (InputStream input = inputStream; try (InputStream input = inputStream;
OutputStream output = response.getOutputStream()) { OutputStream output = response.getOutputStream()) {
if (ranges.isEmpty() || ranges.get(0) == full) { if (ranges.isEmpty() || ranges.get(0) == full) {
// Return full file. // Return full file.
logger.info("Return full file"); log.info("Return full file");
response.setContentType(contentType); response.setContentType(contentType);
response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total); response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, full.start, full.end, full.total));
response.setHeader("Content-Length", String.valueOf(full.length)); response.setHeader(CONTENT_LENGTH, String.valueOf(full.length));
Range.copy(input, output, length, full.start, full.length); Range.copy(input, output, length, full.start, full.length);
} else if (ranges.size() == 1) { } else if (ranges.size() == 1) {
// Return single part of file. // Return single part of file.
Range r = ranges.get(0); Range r = ranges.get(0);
logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end); log.info("Return 1 part of file : from ({}) to ({})", r.start, r.end);
response.setContentType(contentType); response.setContentType(contentType);
response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total); response.setHeader(CONTENT_RANGE, String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
response.setHeader("Content-Length", String.valueOf(r.length)); response.setHeader(CONTENT_LENGTH, String.valueOf(r.length));
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
// Copy single part range. // Copy single part range.
@@ -243,20 +295,20 @@ public class MultipartFileSender {
} else { } else {
// Return multiple parts of file. // Return multiple parts of file.
response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); response.setContentType(CONTENT_TYPE_MULTITYPE_WITH_BOUNDARY);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
// Cast back to ServletOutputStream to get the easy println methods. // Cast back to ServletOutputStream to get the easy println methods.
ServletOutputStream sos = (ServletOutputStream) output; ServletOutputStream sos = (ServletOutputStream) output;
// Copy multi part range. // Copy multi part range.
for (Range r : ranges) { for (Range r : Range.relativize(ranges)) {
logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end); log.info("Return multi part of file : from ({}) to ({})", r.start, r.end);
// Add multipart boundary and header fields for every range. // Add multipart boundary and header fields for every range.
sos.println(); sos.println();
sos.println("--" + MULTIPART_BOUNDARY); sos.println("--" + MULTIPART_BOUNDARY);
sos.println("Content-Type: " + contentType); sos.println(CONTENT_TYPE + ": " + contentType);
sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total); sos.println(CONTENT_RANGE + ": " + String.format(BYTES_RANGE_FORMAT, r.start, r.end, r.total));
// Copy single part range of multi part range. // Copy single part range of multi part range.
Range.copy(input, output, length, r.start, r.length); Range.copy(input, output, length, r.start, r.length);
@@ -268,8 +320,14 @@ public class MultipartFileSender {
} }
} }
} }
private static boolean isNullOrEmpty(String disposition) {
return !(disposition == null || disposition.length() == 0);
}
private static class Range { private static class Range {
long start; long start;
long end; long end;
@@ -278,21 +336,36 @@ public class MultipartFileSender {
/** /**
* Construct a byte range. * Construct a byte range.
*
* @param start Start of the byte range. * @param start Start of the byte range.
* @param end End of the byte range. * @param end End of the byte range.
* @param total Total length of the byte source. * @param total Total length of the byte source.
*/ */
public Range(long start, long end, long total) { public Range(long start, long end, long total) {
this.start = start; this.start = start;
if (end < start + DEFAULT_BUFFER_SIZE) { // if (end <= start + DEFAULT_BUFFER_SIZE) {
this.end = end; this.end = end;
} else { // } else {
this.end = Math.min(start + DEFAULT_BUFFER_SIZE, total - 1); // this.end = Math.min(start + DEFAULT_BUFFER_SIZE, total - 1);
} // }
this.length = this.end - start + 1; this.length = this.end - start + 1;
this.total = total; this.total = total;
} }
private static List<Range> relativize(List<Range> ranges) {
List<Range> builder = new ArrayList<>(ranges.size());
Range prevRange = null;
for (Range r : ranges) {
Range newRange = isNull(prevRange) ? r : new Range(r.start - prevRange.end - 1, r.end - prevRange.end - 1, r.total);
builder.add(newRange);
prevRange = r;
}
return builder;
}
public static long sublong(String value, int beginIndex, int endIndex) { public static long sublong(String value, int beginIndex, int endIndex) {
String substring = value.substring(beginIndex, endIndex); String substring = value.substring(beginIndex, endIndex);
return (substring.length() > 0) ? Long.parseLong(substring) : -1; return (substring.length() > 0) ? Long.parseLong(substring) : -1;
@@ -325,15 +398,8 @@ public class MultipartFileSender {
} }
} }
} }
private static class HttpUtils {
/** private static boolean accepts(String acceptHeader, String toAccept) {
* Returns true if the given accept header accepts the given value.
* @param acceptHeader The accept header.
* @param toAccept The value to be accepted.
* @return True if the given accept header accepts the given value.
*/
public static boolean accepts(String acceptHeader, String toAccept) {
String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*"); String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
Arrays.sort(acceptValues); Arrays.sort(acceptValues);
@@ -342,17 +408,10 @@ public class MultipartFileSender {
|| Arrays.binarySearch(acceptValues, "*/*") > -1; || Arrays.binarySearch(acceptValues, "*/*") > -1;
} }
/** private static boolean matches(String matchHeader, String toMatch) {
* Returns true if the given match header matches the given value.
* @param matchHeader The match header.
* @param toMatch The value to be matched.
* @return True if the given match header matches the given value.
*/
public static boolean matches(String matchHeader, String toMatch) {
String[] matchValues = matchHeader.split("\\s*,\\s*"); String[] matchValues = matchHeader.split("\\s*,\\s*");
Arrays.sort(matchValues); Arrays.sort(matchValues);
return Arrays.binarySearch(matchValues, toMatch) > -1 return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|| Arrays.binarySearch(matchValues, "*") > -1;
}
} }
} }

View File

@@ -1962,6 +1962,8 @@ mail.helpdesk = ${mail.admin}
# Should all Request Copy emails go to the helpdesk instead of the item submitter? # Should all Request Copy emails go to the helpdesk instead of the item submitter?
request.item.helpdesk.override = false request.item.helpdesk.override = false
#bitstream-download.buffer.size = 1000000
#------------------------------------------------------------------# #------------------------------------------------------------------#

View File

@@ -736,4 +736,22 @@
<extension>epub</extension> <extension>epub</extension>
</bitstream-type> </bitstream-type>
<bitstream-type>
<mimetype>video/mp4</mimetype>
<short_description>mp4</short_description>
<description>mpeg4</description>
<support_level>1</support_level>
<internal>false</internal>
<extension>mp4</extension>
</bitstream-type>
<bitstream-type>
<mimetype>audio/mpeg</mimetype>
<short_description>mp3</short_description>
<description>MPEG audio</description>
<support_level>1</support_level>
<internal>false</internal>
<extension>mp3</extension>
</bitstream-type>
</dspace-bitstream-types> </dspace-bitstream-types>