mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-12 04:23:13 +00:00
DS-3651: several improvements
This commit is contained in:
@@ -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.
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@@ -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
|
||||||
|
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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,34 +398,20 @@ 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.
|
String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
|
||||||
* @param acceptHeader The accept header.
|
Arrays.sort(acceptValues);
|
||||||
* @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*");
|
|
||||||
Arrays.sort(acceptValues);
|
|
||||||
|
|
||||||
return Arrays.binarySearch(acceptValues, toAccept) > -1
|
return Arrays.binarySearch(acceptValues, toAccept) > -1
|
||||||
|| Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
|
|| Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
|
||||||
|| Arrays.binarySearch(acceptValues, "*/*") > -1;
|
|| Arrays.binarySearch(acceptValues, "*/*") > -1;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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*");
|
|
||||||
Arrays.sort(matchValues);
|
|
||||||
return Arrays.binarySearch(matchValues, toMatch) > -1
|
|
||||||
|| Arrays.binarySearch(matchValues, "*") > -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean matches(String matchHeader, String toMatch) {
|
||||||
|
String[] matchValues = matchHeader.split("\\s*,\\s*");
|
||||||
|
Arrays.sort(matchValues);
|
||||||
|
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------------#
|
#------------------------------------------------------------------#
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user